modality-ts 0.0.7 → 0.0.9
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/dist/check/diagnostics/bounds.d.ts +4 -0
- package/dist/check/diagnostics/bounds.d.ts.map +1 -0
- package/dist/check/diagnostics/bounds.js +25 -0
- package/dist/check/diagnostics/bounds.js.map +1 -0
- package/dist/check/diagnostics/vacuity.d.ts +3 -0
- package/dist/check/diagnostics/vacuity.d.ts.map +1 -0
- package/dist/check/diagnostics/vacuity.js +22 -0
- package/dist/check/diagnostics/vacuity.js.map +1 -0
- package/dist/check/engine/check-model.d.ts +4 -0
- package/dist/check/engine/check-model.d.ts.map +1 -0
- package/dist/check/engine/check-model.js +162 -0
- package/dist/check/engine/check-model.js.map +1 -0
- package/dist/check/engine/initial-states.d.ts +3 -0
- package/dist/check/engine/initial-states.d.ts.map +1 -0
- package/dist/check/engine/initial-states.js +11 -0
- package/dist/check/engine/initial-states.js.map +1 -0
- package/dist/check/engine/model-api.d.ts +4 -0
- package/dist/check/engine/model-api.d.ts.map +1 -0
- package/dist/check/engine/model-api.js +15 -0
- package/dist/check/engine/model-api.js.map +1 -0
- package/dist/check/engine/mounts.d.ts +3 -0
- package/dist/check/engine/mounts.d.ts.map +1 -0
- package/dist/check/engine/mounts.js +13 -0
- package/dist/check/engine/mounts.js.map +1 -0
- package/dist/check/engine/stabilize.d.ts +3 -0
- package/dist/check/engine/stabilize.d.ts.map +1 -0
- package/dist/check/engine/stabilize.js +82 -0
- package/dist/check/engine/stabilize.js.map +1 -0
- package/dist/check/engine/state-utils.d.ts +12 -0
- package/dist/check/engine/state-utils.d.ts.map +1 -0
- package/dist/check/engine/state-utils.js +30 -0
- package/dist/check/engine/state-utils.js.map +1 -0
- package/dist/check/engine/transitions.d.ts +4 -0
- package/dist/check/engine/transitions.d.ts.map +1 -0
- package/dist/check/engine/transitions.js +15 -0
- package/dist/check/engine/transitions.js.map +1 -0
- package/dist/check/index.d.ts +14 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +13 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/properties/checked-state.d.ts +3 -0
- package/dist/check/properties/checked-state.d.ts.map +1 -0
- package/dist/check/properties/checked-state.js +21 -0
- package/dist/check/properties/checked-state.js.map +1 -0
- package/dist/check/properties/finalize.d.ts +4 -0
- package/dist/check/properties/finalize.d.ts.map +1 -0
- package/dist/check/properties/finalize.js +71 -0
- package/dist/check/properties/finalize.js.map +1 -0
- package/dist/check/properties/leads-to.d.ts +8 -0
- package/dist/check/properties/leads-to.d.ts.map +1 -0
- package/dist/check/properties/leads-to.js +69 -0
- package/dist/check/properties/leads-to.js.map +1 -0
- package/dist/check/properties/observe.d.ts +5 -0
- package/dist/check/properties/observe.d.ts.map +1 -0
- package/dist/check/properties/observe.js +56 -0
- package/dist/check/properties/observe.js.map +1 -0
- package/dist/check/properties/reachable-from.d.ts +8 -0
- package/dist/check/properties/reachable-from.d.ts.map +1 -0
- package/dist/check/properties/reachable-from.js +19 -0
- package/dist/check/properties/reachable-from.js.map +1 -0
- package/dist/check/runtime/domains.d.ts +5 -0
- package/dist/check/runtime/domains.d.ts.map +1 -0
- package/dist/check/runtime/domains.js +53 -0
- package/dist/check/runtime/domains.js.map +1 -0
- package/dist/check/runtime/effects.d.ts +9 -0
- package/dist/check/runtime/effects.d.ts.map +1 -0
- package/dist/check/runtime/effects.js +86 -0
- package/dist/check/runtime/effects.js.map +1 -0
- package/dist/check/runtime/expr.d.ts +7 -0
- package/dist/check/runtime/expr.d.ts.map +1 -0
- package/dist/check/runtime/expr.js +49 -0
- package/dist/check/runtime/expr.js.map +1 -0
- package/dist/check/runtime/navigation.d.ts +7 -0
- package/dist/check/runtime/navigation.d.ts.map +1 -0
- package/dist/check/runtime/navigation.js +60 -0
- package/dist/check/runtime/navigation.js.map +1 -0
- package/dist/check/runtime/opaque.d.ts +3 -0
- package/dist/check/runtime/opaque.d.ts.map +1 -0
- package/dist/check/runtime/opaque.js +72 -0
- package/dist/check/runtime/opaque.js.map +1 -0
- package/dist/check/runtime/paths.d.ts +4 -0
- package/dist/check/runtime/paths.d.ts.map +1 -0
- package/dist/check/runtime/paths.js +28 -0
- package/dist/check/runtime/paths.js.map +1 -0
- package/dist/check/runtime/pending.d.ts +9 -0
- package/dist/check/runtime/pending.d.ts.map +1 -0
- package/dist/check/runtime/pending.js +5 -0
- package/dist/check/runtime/pending.js.map +1 -0
- package/dist/check/runtime/tokens.d.ts +7 -0
- package/dist/check/runtime/tokens.d.ts.map +1 -0
- package/dist/check/runtime/tokens.js +36 -0
- package/dist/check/runtime/tokens.js.map +1 -0
- package/dist/check/slicing/slice-model.d.ts +5 -0
- package/dist/check/slicing/slice-model.d.ts.map +1 -0
- package/dist/check/slicing/slice-model.js +48 -0
- package/dist/check/slicing/slice-model.js.map +1 -0
- package/dist/check/traces/step-facts.d.ts +3 -0
- package/dist/check/traces/step-facts.d.ts.map +1 -0
- package/dist/check/traces/step-facts.js +35 -0
- package/dist/check/traces/step-facts.js.map +1 -0
- package/dist/check/traces/trace.d.ts +8 -0
- package/dist/check/traces/trace.d.ts.map +1 -0
- package/dist/check/traces/trace.js +42 -0
- package/dist/check/traces/trace.js.map +1 -0
- package/dist/{checker/search/index.d.ts → check/types.d.ts} +16 -6
- package/dist/check/types.d.ts.map +1 -0
- package/dist/{kernel/ir → check}/types.js.map +1 -1
- package/dist/cli/check.d.ts +3 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +2 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/ci.d.ts.map +1 -0
- package/dist/cli/ci.js.map +1 -0
- package/dist/{modality → cli}/cli.d.ts.map +1 -1
- package/dist/{modality → cli}/cli.js +131 -18
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/codegen/model.d.ts +3 -0
- package/dist/cli/codegen/model.d.ts.map +1 -0
- package/dist/{modality → cli}/codegen/model.js +18 -10
- package/dist/cli/codegen/model.js.map +1 -0
- package/dist/{modality → cli}/codegen/replay-test.d.ts +1 -1
- package/dist/cli/codegen/replay-test.d.ts.map +1 -0
- package/dist/{modality → cli}/codegen/replay-test.js +12 -12
- package/dist/cli/codegen/replay-test.js.map +1 -0
- package/dist/cli/conform.d.ts +3 -0
- package/dist/cli/conform.d.ts.map +1 -0
- package/dist/cli/conform.js +2 -0
- package/dist/cli/conform.js.map +1 -0
- package/dist/cli/export-tla.d.ts +3 -0
- package/dist/cli/export-tla.d.ts.map +1 -0
- package/dist/cli/export-tla.js +2 -0
- package/dist/cli/export-tla.js.map +1 -0
- package/dist/{modality → cli}/extract.d.ts +1 -1
- package/dist/cli/extract.d.ts.map +1 -0
- package/dist/cli/extract.js.map +1 -0
- package/dist/{modality → cli}/features/check/command.d.ts +2 -2
- package/dist/cli/features/check/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/check/command.js +53 -17
- package/dist/cli/features/check/command.js.map +1 -0
- package/dist/cli/features/check/index.d.ts +3 -0
- package/dist/cli/features/check/index.d.ts.map +1 -0
- package/dist/cli/features/check/index.js +2 -0
- package/dist/cli/features/check/index.js.map +1 -0
- package/dist/cli/features/ci/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/ci/command.js +66 -26
- package/dist/cli/features/ci/command.js.map +1 -0
- package/dist/cli/features/ci/index.d.ts.map +1 -0
- package/dist/cli/features/ci/index.js.map +1 -0
- package/dist/{modality → cli}/features/conform/command.d.ts +1 -1
- package/dist/cli/features/conform/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/conform/command.js +67 -25
- package/dist/cli/features/conform/command.js.map +1 -0
- package/dist/cli/features/conform/index.d.ts.map +1 -0
- package/dist/cli/features/conform/index.js.map +1 -0
- package/dist/{modality → cli}/features/export/command.d.ts +1 -1
- package/dist/cli/features/export/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/export/command.js +72 -27
- package/dist/cli/features/export/command.js.map +1 -0
- package/dist/{modality → cli}/features/export/index.d.ts +2 -2
- package/dist/cli/features/export/index.d.ts.map +1 -0
- package/dist/cli/features/export/index.js +2 -0
- package/dist/cli/features/export/index.js.map +1 -0
- package/dist/{modality → cli}/features/extract/command.d.ts +3 -3
- package/dist/cli/features/extract/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/extract/command.js +219 -77
- package/dist/cli/features/extract/command.js.map +1 -0
- package/dist/{modality → cli}/features/extract/index.d.ts +1 -1
- package/dist/cli/features/extract/index.d.ts.map +1 -0
- package/dist/cli/features/extract/index.js.map +1 -0
- package/dist/{modality → cli}/features/replay/command.d.ts +1 -1
- package/dist/cli/features/replay/command.d.ts.map +1 -0
- package/dist/{modality → cli}/features/replay/command.js +42 -16
- package/dist/cli/features/replay/command.js.map +1 -0
- package/dist/cli/features/replay/index.d.ts.map +1 -0
- package/dist/cli/features/replay/index.js.map +1 -0
- package/dist/{harness → cli/harness}/index.d.ts +1 -1
- package/dist/cli/harness/index.d.ts.map +1 -0
- package/dist/{harness → cli/harness}/index.js +128 -37
- package/dist/cli/harness/index.js.map +1 -0
- package/dist/{modality → cli}/overlay.d.ts +1 -1
- package/dist/cli/overlay.d.ts.map +1 -0
- package/dist/{modality → cli}/overlay.js +1 -1
- package/dist/cli/overlay.js.map +1 -0
- package/dist/{modality → cli}/registry/index.d.ts +2 -2
- package/dist/cli/registry/index.d.ts.map +1 -0
- package/dist/{modality → cli}/registry/index.js +32 -17
- package/dist/cli/registry/index.js.map +1 -0
- package/dist/cli/replay.d.ts +3 -0
- package/dist/cli/replay.d.ts.map +1 -0
- package/dist/cli/replay.js.map +1 -0
- package/dist/{runtime → cli/runtime}/index.d.ts +1 -1
- package/dist/cli/runtime/index.d.ts.map +1 -0
- package/dist/{runtime → cli/runtime}/index.js +37 -10
- package/dist/cli/runtime/index.js.map +1 -0
- package/dist/core/artifacts/index.d.ts.map +1 -0
- package/dist/{kernel → core}/artifacts/index.js +7 -2
- package/dist/core/artifacts/index.js.map +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/ir/canonical.d.ts.map +1 -0
- package/dist/{kernel → core}/ir/canonical.js +30 -7
- package/dist/core/ir/canonical.js.map +1 -0
- package/dist/core/ir/domains.d.ts.map +1 -0
- package/dist/{kernel → core}/ir/domains.js +18 -6
- package/dist/core/ir/domains.js.map +1 -0
- package/dist/core/ir/types.d.ts.map +1 -0
- package/dist/{kernel/trace → core/ir}/types.js.map +1 -1
- package/dist/core/ir/validator.d.ts.map +1 -0
- package/dist/{kernel → core}/ir/validator.js +48 -19
- package/dist/core/ir/validator.js.map +1 -0
- package/dist/core/overlay/index.d.ts.map +1 -0
- package/dist/{kernel → core}/overlay/index.js +35 -14
- package/dist/core/overlay/index.js.map +1 -0
- package/dist/core/props/index.d.ts.map +1 -0
- package/dist/{kernel → core}/props/index.js +69 -21
- package/dist/core/props/index.js.map +1 -0
- package/dist/{kernel → core}/report/types.d.ts.map +1 -1
- package/dist/{kernel → core}/report/types.js.map +1 -1
- package/dist/core/trace/types.d.ts.map +1 -0
- package/dist/core/trace/types.js +2 -0
- package/dist/core/trace/types.js.map +1 -0
- package/dist/extract/engine/index.d.ts +4 -0
- package/dist/extract/engine/index.d.ts.map +1 -0
- package/dist/extract/engine/index.js +4 -0
- package/dist/extract/engine/index.js.map +1 -0
- package/dist/{extraction → extract/engine}/pipeline/index.d.ts +1 -2
- package/dist/extract/engine/pipeline/index.d.ts.map +1 -0
- package/dist/{extraction → extract/engine}/pipeline/index.js +55 -18
- package/dist/extract/engine/pipeline/index.js.map +1 -0
- package/dist/{extraction → extract/engine}/spi/index.d.ts +17 -1
- package/dist/extract/engine/spi/index.d.ts.map +1 -0
- package/dist/extract/engine/spi/index.js.map +1 -0
- package/dist/extract/engine/ts/ast.d.ts +23 -0
- package/dist/extract/engine/ts/ast.d.ts.map +1 -0
- package/dist/extract/engine/ts/ast.js +97 -0
- package/dist/extract/engine/ts/ast.js.map +1 -0
- package/dist/extract/engine/ts/components.d.ts +21 -0
- package/dist/extract/engine/ts/components.d.ts.map +1 -0
- package/dist/extract/engine/ts/components.js +287 -0
- package/dist/extract/engine/ts/components.js.map +1 -0
- package/dist/extract/engine/ts/context.d.ts +10 -0
- package/dist/extract/engine/ts/context.d.ts.map +1 -0
- package/dist/extract/engine/ts/context.js +273 -0
- package/dist/extract/engine/ts/context.js.map +1 -0
- package/dist/extract/engine/ts/domains.d.ts +8 -0
- package/dist/extract/engine/ts/domains.d.ts.map +1 -0
- package/dist/extract/engine/ts/domains.js +262 -0
- package/dist/extract/engine/ts/domains.js.map +1 -0
- package/dist/extract/engine/ts/expressions.d.ts +2 -0
- package/dist/extract/engine/ts/expressions.d.ts.map +1 -0
- package/dist/extract/engine/ts/expressions.js +2 -0
- package/dist/extract/engine/ts/expressions.js.map +1 -0
- package/dist/extract/engine/ts/handlers.d.ts +4 -0
- package/dist/extract/engine/ts/handlers.d.ts.map +1 -0
- package/dist/extract/engine/ts/handlers.js +3 -0
- package/dist/extract/engine/ts/handlers.js.map +1 -0
- package/dist/extract/engine/ts/ids.d.ts +7 -0
- package/dist/extract/engine/ts/ids.d.ts.map +1 -0
- package/dist/extract/engine/ts/ids.js +63 -0
- package/dist/extract/engine/ts/ids.js.map +1 -0
- package/dist/extract/engine/ts/input-transitions.d.ts +5 -0
- package/dist/extract/engine/ts/input-transitions.d.ts.map +1 -0
- package/dist/extract/engine/ts/input-transitions.js +132 -0
- package/dist/extract/engine/ts/input-transitions.js.map +1 -0
- package/dist/extract/engine/ts/jsx.d.ts +3 -0
- package/dist/extract/engine/ts/jsx.d.ts.map +1 -0
- package/dist/extract/engine/ts/jsx.js +3 -0
- package/dist/extract/engine/ts/jsx.js.map +1 -0
- package/dist/extract/engine/ts/react-source-transitions.d.ts +24 -0
- package/dist/extract/engine/ts/react-source-transitions.d.ts.map +1 -0
- package/dist/extract/engine/ts/react-source-transitions.js +243 -0
- package/dist/extract/engine/ts/react-source-transitions.js.map +1 -0
- package/dist/extract/engine/ts/routes.d.ts +9 -0
- package/dist/extract/engine/ts/routes.d.ts.map +1 -0
- package/dist/extract/engine/ts/routes.js +94 -0
- package/dist/extract/engine/ts/routes.js.map +1 -0
- package/dist/extract/engine/ts/source.d.ts +3 -0
- package/dist/extract/engine/ts/source.d.ts.map +1 -0
- package/dist/extract/engine/ts/source.js +5 -0
- package/dist/extract/engine/ts/source.js.map +1 -0
- package/dist/extract/engine/ts/static-navigation.d.ts +5 -0
- package/dist/extract/engine/ts/static-navigation.d.ts.map +1 -0
- package/dist/extract/engine/ts/static-navigation.js +425 -0
- package/dist/extract/engine/ts/static-navigation.js.map +1 -0
- package/dist/extract/engine/ts/transition/async.d.ts +44 -0
- package/dist/extract/engine/ts/transition/async.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/async.js +546 -0
- package/dist/extract/engine/ts/transition/async.js.map +1 -0
- package/dist/extract/engine/ts/transition/component-props.d.ts +26 -0
- package/dist/extract/engine/ts/transition/component-props.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/component-props.js +200 -0
- package/dist/extract/engine/ts/transition/component-props.js.map +1 -0
- package/dist/extract/engine/ts/transition/effects.d.ts +33 -0
- package/dist/extract/engine/ts/transition/effects.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/effects.js +303 -0
- package/dist/extract/engine/ts/transition/effects.js.map +1 -0
- package/dist/extract/engine/ts/transition/expressions.d.ts +33 -0
- package/dist/extract/engine/ts/transition/expressions.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/expressions.js +353 -0
- package/dist/extract/engine/ts/transition/expressions.js.map +1 -0
- package/dist/extract/engine/ts/transition/guards.d.ts +21 -0
- package/dist/extract/engine/ts/transition/guards.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/guards.js +249 -0
- package/dist/extract/engine/ts/transition/guards.js.map +1 -0
- package/dist/extract/engine/ts/transition/handlers.d.ts +53 -0
- package/dist/extract/engine/ts/transition/handlers.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/handlers.js +438 -0
- package/dist/extract/engine/ts/transition/handlers.js.map +1 -0
- package/dist/extract/engine/ts/transition/locals.d.ts +16 -0
- package/dist/extract/engine/ts/transition/locals.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/locals.js +113 -0
- package/dist/extract/engine/ts/transition/locals.js.map +1 -0
- package/dist/extract/engine/ts/transition/navigation.d.ts +21 -0
- package/dist/extract/engine/ts/transition/navigation.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/navigation.js +129 -0
- package/dist/extract/engine/ts/transition/navigation.js.map +1 -0
- package/dist/extract/engine/ts/transition/plugin-calls.d.ts +10 -0
- package/dist/extract/engine/ts/transition/plugin-calls.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/plugin-calls.js +114 -0
- package/dist/extract/engine/ts/transition/plugin-calls.js.map +1 -0
- package/dist/extract/engine/ts/transition/timers.d.ts +15 -0
- package/dist/extract/engine/ts/transition/timers.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/timers.js +121 -0
- package/dist/extract/engine/ts/transition/timers.js.map +1 -0
- package/dist/extract/engine/ts/transition/ui.d.ts +12 -0
- package/dist/extract/engine/ts/transition/ui.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/ui.js +113 -0
- package/dist/extract/engine/ts/transition/ui.js.map +1 -0
- package/dist/extract/engine/ts/traversal.d.ts +3 -0
- package/dist/extract/engine/ts/traversal.d.ts.map +1 -0
- package/dist/extract/engine/ts/traversal.js +6 -0
- package/dist/extract/engine/ts/traversal.js.map +1 -0
- package/dist/extract/engine/ts/types.d.ts +46 -0
- package/dist/extract/engine/ts/types.d.ts.map +1 -0
- package/dist/extract/engine/ts/types.js +2 -0
- package/dist/extract/engine/ts/types.js.map +1 -0
- package/dist/extract/index.d.ts +2 -0
- package/dist/extract/index.d.ts.map +1 -0
- package/dist/extract/index.js +2 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/sources/jotai/discover.d.ts +3 -0
- package/dist/extract/sources/jotai/discover.d.ts.map +1 -0
- package/dist/extract/sources/jotai/discover.js +65 -0
- package/dist/extract/sources/jotai/discover.js.map +1 -0
- package/dist/extract/sources/jotai/domains.d.ts +6 -0
- package/dist/extract/sources/jotai/domains.d.ts.map +1 -0
- package/dist/extract/sources/jotai/domains.js +283 -0
- package/dist/extract/sources/jotai/domains.js.map +1 -0
- package/dist/{sources → extract/sources}/jotai/harness.d.ts +2 -2
- package/dist/extract/sources/jotai/harness.d.ts.map +1 -0
- package/dist/{sources → extract/sources}/jotai/harness.js +1 -1
- package/dist/extract/sources/jotai/harness.js.map +1 -0
- package/dist/extract/sources/jotai/index.d.ts +4 -0
- package/dist/extract/sources/jotai/index.d.ts.map +1 -0
- package/dist/extract/sources/jotai/index.js +19 -0
- package/dist/extract/sources/jotai/index.js.map +1 -0
- package/dist/extract/sources/jotai/writes.d.ts +8 -0
- package/dist/extract/sources/jotai/writes.d.ts.map +1 -0
- package/dist/extract/sources/jotai/writes.js +173 -0
- package/dist/extract/sources/jotai/writes.js.map +1 -0
- package/dist/{sources → extract/sources}/router/harness.d.ts +1 -1
- package/dist/extract/sources/router/harness.d.ts.map +1 -0
- package/dist/{sources → extract/sources}/router/harness.js +6 -2
- package/dist/extract/sources/router/harness.js.map +1 -0
- package/dist/extract/sources/router/index.d.ts +9 -0
- package/dist/extract/sources/router/index.d.ts.map +1 -0
- package/dist/extract/sources/router/index.js +22 -0
- package/dist/extract/sources/router/index.js.map +1 -0
- package/dist/extract/sources/router/navigation.d.ts +5 -0
- package/dist/extract/sources/router/navigation.d.ts.map +1 -0
- package/dist/extract/sources/router/navigation.js +16 -0
- package/dist/extract/sources/router/navigation.js.map +1 -0
- package/dist/extract/sources/router/routes.d.ts +4 -0
- package/dist/extract/sources/router/routes.d.ts.map +1 -0
- package/dist/extract/sources/router/routes.js +28 -0
- package/dist/extract/sources/router/routes.js.map +1 -0
- package/dist/extract/sources/swr/discover.d.ts +11 -0
- package/dist/extract/sources/swr/discover.d.ts.map +1 -0
- package/dist/extract/sources/swr/discover.js +175 -0
- package/dist/extract/sources/swr/discover.js.map +1 -0
- package/dist/extract/sources/swr/domains.d.ts +5 -0
- package/dist/extract/sources/swr/domains.d.ts.map +1 -0
- package/dist/extract/sources/swr/domains.js +99 -0
- package/dist/extract/sources/swr/domains.js.map +1 -0
- package/dist/{sources → extract/sources}/swr/harness.d.ts +2 -2
- package/dist/extract/sources/swr/harness.d.ts.map +1 -0
- package/dist/{sources → extract/sources}/swr/harness.js +3 -1
- package/dist/extract/sources/swr/harness.js.map +1 -0
- package/dist/extract/sources/swr/index.d.ts +6 -0
- package/dist/extract/sources/swr/index.d.ts.map +1 -0
- package/dist/extract/sources/swr/index.js +22 -0
- package/dist/extract/sources/swr/index.js.map +1 -0
- package/dist/{sources/swr/index.d.ts → extract/sources/swr/template.d.ts} +4 -6
- package/dist/extract/sources/swr/template.d.ts.map +1 -0
- package/dist/extract/sources/swr/template.js +331 -0
- package/dist/extract/sources/swr/template.js.map +1 -0
- package/dist/extract/sources/swr/writes.d.ts +3 -0
- package/dist/extract/sources/swr/writes.d.ts.map +1 -0
- package/dist/extract/sources/swr/writes.js +54 -0
- package/dist/extract/sources/swr/writes.js.map +1 -0
- package/dist/{sources → extract/sources}/use-state/harness.d.ts +2 -2
- package/dist/extract/sources/use-state/harness.d.ts.map +1 -0
- package/dist/{sources → extract/sources}/use-state/harness.js +1 -1
- package/dist/extract/sources/use-state/harness.js.map +1 -0
- package/dist/extract/sources/use-state/index.d.ts +4 -0
- package/dist/extract/sources/use-state/index.d.ts.map +1 -0
- package/dist/{sources → extract/sources}/use-state/index.js +92 -35
- package/dist/extract/sources/use-state/index.js.map +1 -0
- package/dist/extract/sources/use-state/transitions.d.ts +4 -0
- package/dist/extract/sources/use-state/transitions.d.ts.map +1 -0
- package/dist/extract/sources/use-state/transitions.js +20 -0
- package/dist/extract/sources/use-state/transitions.js.map +1 -0
- package/dist/extract/sources/use-state/types.d.ts +67 -0
- package/dist/extract/sources/use-state/types.d.ts.map +1 -0
- package/dist/extract/sources/use-state/types.js +2 -0
- package/dist/extract/sources/use-state/types.js.map +1 -0
- package/package.json +85 -71
- package/dist/checker/encode/index.d.ts +0 -2
- package/dist/checker/encode/index.d.ts.map +0 -1
- package/dist/checker/encode/index.js +0 -2
- package/dist/checker/encode/index.js.map +0 -1
- package/dist/checker/index.d.ts +0 -6
- package/dist/checker/index.d.ts.map +0 -1
- package/dist/checker/index.js +0 -6
- package/dist/checker/index.js.map +0 -1
- package/dist/checker/monitors/index.d.ts +0 -2
- package/dist/checker/monitors/index.d.ts.map +0 -1
- package/dist/checker/monitors/index.js.map +0 -1
- package/dist/checker/search/eval.d.ts +0 -16
- package/dist/checker/search/eval.d.ts.map +0 -1
- package/dist/checker/search/eval.js +0 -308
- package/dist/checker/search/eval.js.map +0 -1
- package/dist/checker/search/index.d.ts.map +0 -1
- package/dist/checker/search/index.js +0 -556
- package/dist/checker/search/index.js.map +0 -1
- package/dist/checker/slicing/index.d.ts +0 -2
- package/dist/checker/slicing/index.d.ts.map +0 -1
- package/dist/checker/slicing/index.js +0 -2
- package/dist/checker/slicing/index.js.map +0 -1
- package/dist/checker/traces/index.d.ts +0 -2
- package/dist/checker/traces/index.d.ts.map +0 -1
- package/dist/checker/traces/index.js +0 -2
- package/dist/checker/traces/index.js.map +0 -1
- package/dist/extraction/index.d.ts +0 -34
- package/dist/extraction/index.d.ts.map +0 -1
- package/dist/extraction/index.js +0 -2977
- package/dist/extraction/index.js.map +0 -1
- package/dist/extraction/pipeline/index.d.ts.map +0 -1
- package/dist/extraction/pipeline/index.js.map +0 -1
- package/dist/extraction/spi/index.d.ts.map +0 -1
- package/dist/extraction/spi/index.js +0 -2
- package/dist/extraction/spi/index.js.map +0 -1
- package/dist/harness/index.d.ts.map +0 -1
- package/dist/harness/index.js.map +0 -1
- package/dist/kernel/artifacts/index.d.ts.map +0 -1
- package/dist/kernel/artifacts/index.js.map +0 -1
- package/dist/kernel/index.d.ts.map +0 -1
- package/dist/kernel/index.js.map +0 -1
- package/dist/kernel/ir/canonical.d.ts.map +0 -1
- package/dist/kernel/ir/canonical.js.map +0 -1
- package/dist/kernel/ir/domains.d.ts.map +0 -1
- package/dist/kernel/ir/domains.js.map +0 -1
- package/dist/kernel/ir/types.d.ts.map +0 -1
- package/dist/kernel/ir/validator.d.ts.map +0 -1
- package/dist/kernel/ir/validator.js.map +0 -1
- package/dist/kernel/overlay/index.d.ts.map +0 -1
- package/dist/kernel/overlay/index.js.map +0 -1
- package/dist/kernel/props/index.d.ts.map +0 -1
- package/dist/kernel/props/index.js.map +0 -1
- package/dist/kernel/trace/types.d.ts.map +0 -1
- package/dist/modality/check.d.ts +0 -3
- package/dist/modality/check.d.ts.map +0 -1
- package/dist/modality/check.js +0 -2
- package/dist/modality/check.js.map +0 -1
- package/dist/modality/ci.d.ts.map +0 -1
- package/dist/modality/ci.js.map +0 -1
- package/dist/modality/cli.js.map +0 -1
- package/dist/modality/codegen/model.d.ts +0 -3
- package/dist/modality/codegen/model.d.ts.map +0 -1
- package/dist/modality/codegen/model.js.map +0 -1
- package/dist/modality/codegen/replay-test.d.ts.map +0 -1
- package/dist/modality/codegen/replay-test.js.map +0 -1
- package/dist/modality/conform.d.ts +0 -3
- package/dist/modality/conform.d.ts.map +0 -1
- package/dist/modality/conform.js +0 -2
- package/dist/modality/conform.js.map +0 -1
- package/dist/modality/export-tla.d.ts +0 -3
- package/dist/modality/export-tla.d.ts.map +0 -1
- package/dist/modality/export-tla.js +0 -2
- package/dist/modality/export-tla.js.map +0 -1
- package/dist/modality/extract.d.ts.map +0 -1
- package/dist/modality/extract.js.map +0 -1
- package/dist/modality/features/check/command.d.ts.map +0 -1
- package/dist/modality/features/check/command.js.map +0 -1
- package/dist/modality/features/check/index.d.ts +0 -3
- package/dist/modality/features/check/index.d.ts.map +0 -1
- package/dist/modality/features/check/index.js +0 -2
- package/dist/modality/features/check/index.js.map +0 -1
- package/dist/modality/features/ci/command.d.ts.map +0 -1
- package/dist/modality/features/ci/command.js.map +0 -1
- package/dist/modality/features/ci/index.d.ts.map +0 -1
- package/dist/modality/features/ci/index.js.map +0 -1
- package/dist/modality/features/conform/command.d.ts.map +0 -1
- package/dist/modality/features/conform/command.js.map +0 -1
- package/dist/modality/features/conform/index.d.ts.map +0 -1
- package/dist/modality/features/conform/index.js.map +0 -1
- package/dist/modality/features/export/command.d.ts.map +0 -1
- package/dist/modality/features/export/command.js.map +0 -1
- package/dist/modality/features/export/index.d.ts.map +0 -1
- package/dist/modality/features/export/index.js +0 -2
- package/dist/modality/features/export/index.js.map +0 -1
- package/dist/modality/features/extract/command.d.ts.map +0 -1
- package/dist/modality/features/extract/command.js.map +0 -1
- package/dist/modality/features/extract/index.d.ts.map +0 -1
- package/dist/modality/features/extract/index.js.map +0 -1
- package/dist/modality/features/replay/command.d.ts.map +0 -1
- package/dist/modality/features/replay/command.js.map +0 -1
- package/dist/modality/features/replay/index.d.ts.map +0 -1
- package/dist/modality/features/replay/index.js.map +0 -1
- package/dist/modality/overlay.d.ts.map +0 -1
- package/dist/modality/overlay.js.map +0 -1
- package/dist/modality/registry/index.d.ts.map +0 -1
- package/dist/modality/registry/index.js.map +0 -1
- package/dist/modality/replay.d.ts +0 -3
- package/dist/modality/replay.d.ts.map +0 -1
- package/dist/modality/replay.js.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js.map +0 -1
- package/dist/sources/jotai/harness.d.ts.map +0 -1
- package/dist/sources/jotai/harness.js.map +0 -1
- package/dist/sources/jotai/index.d.ts +0 -11
- package/dist/sources/jotai/index.d.ts.map +0 -1
- package/dist/sources/jotai/index.js +0 -428
- package/dist/sources/jotai/index.js.map +0 -1
- package/dist/sources/router/harness.d.ts.map +0 -1
- package/dist/sources/router/harness.js.map +0 -1
- package/dist/sources/router/index.d.ts +0 -15
- package/dist/sources/router/index.d.ts.map +0 -1
- package/dist/sources/router/index.js +0 -41
- package/dist/sources/router/index.js.map +0 -1
- package/dist/sources/swr/harness.d.ts.map +0 -1
- package/dist/sources/swr/harness.js.map +0 -1
- package/dist/sources/swr/index.d.ts.map +0 -1
- package/dist/sources/swr/index.js +0 -528
- package/dist/sources/swr/index.js.map +0 -1
- package/dist/sources/use-state/harness.d.ts.map +0 -1
- package/dist/sources/use-state/harness.js.map +0 -1
- package/dist/sources/use-state/index.d.ts +0 -9
- package/dist/sources/use-state/index.d.ts.map +0 -1
- package/dist/sources/use-state/index.js.map +0 -1
- /package/dist/{kernel/ir → check}/types.js +0 -0
- /package/dist/{modality → cli}/ci.d.ts +0 -0
- /package/dist/{modality → cli}/ci.js +0 -0
- /package/dist/{modality → cli}/cli.d.ts +0 -0
- /package/dist/{modality → cli}/extract.js +0 -0
- /package/dist/{modality → cli}/features/ci/command.d.ts +0 -0
- /package/dist/{modality → cli}/features/ci/index.d.ts +0 -0
- /package/dist/{modality → cli}/features/ci/index.js +0 -0
- /package/dist/{modality → cli}/features/conform/index.d.ts +0 -0
- /package/dist/{modality → cli}/features/conform/index.js +0 -0
- /package/dist/{modality → cli}/features/extract/index.js +0 -0
- /package/dist/{modality → cli}/features/replay/index.d.ts +0 -0
- /package/dist/{modality → cli}/features/replay/index.js +0 -0
- /package/dist/{modality → cli}/replay.js +0 -0
- /package/dist/{kernel → core}/artifacts/index.d.ts +0 -0
- /package/dist/{kernel → core}/index.d.ts +0 -0
- /package/dist/{kernel → core}/index.js +0 -0
- /package/dist/{kernel → core}/ir/canonical.d.ts +0 -0
- /package/dist/{kernel → core}/ir/domains.d.ts +0 -0
- /package/dist/{kernel → core}/ir/types.d.ts +0 -0
- /package/dist/{kernel/report → core/ir}/types.js +0 -0
- /package/dist/{kernel → core}/ir/validator.d.ts +0 -0
- /package/dist/{kernel → core}/overlay/index.d.ts +0 -0
- /package/dist/{kernel → core}/props/index.d.ts +0 -0
- /package/dist/{kernel → core}/report/types.d.ts +0 -0
- /package/dist/{kernel/trace → core/report}/types.js +0 -0
- /package/dist/{kernel → core}/trace/types.d.ts +0 -0
- /package/dist/{checker/monitors → extract/engine/spi}/index.js +0 -0
package/dist/extraction/index.js
DELETED
|
@@ -1,2977 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import { effectReads, effectWrites } from "modality-ts/kernel";
|
|
3
|
-
export * from "./pipeline/index.js";
|
|
4
|
-
export * from "./spi/index.js";
|
|
5
|
-
function emptyContextBindings() {
|
|
6
|
-
return { vars: [], setters: new Map(), hookReturns: new Map() };
|
|
7
|
-
}
|
|
8
|
-
function setterBindingFromDecl(decl) {
|
|
9
|
-
const localMatch = /^local:([^.]+)\.(.+)$/.exec(decl.id);
|
|
10
|
-
const atomMatch = /^atom:(.+)$/.exec(decl.id);
|
|
11
|
-
return {
|
|
12
|
-
varId: decl.id,
|
|
13
|
-
component: localMatch?.[1] ?? "Anonymous",
|
|
14
|
-
stateName: localMatch?.[2] ?? atomMatch?.[1] ?? decl.id,
|
|
15
|
-
domain: decl.domain
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function bindSetter(setters, symbolName, setter) {
|
|
19
|
-
setters.set(scopedSetterKey(setter.component, symbolName), setter);
|
|
20
|
-
const current = setters.get(symbolName);
|
|
21
|
-
if (!current || current.varId === setter.varId) {
|
|
22
|
-
setters.set(symbolName, setter);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
setters.delete(symbolName);
|
|
26
|
-
}
|
|
27
|
-
function scopedSetterKey(component, symbolName) {
|
|
28
|
-
return `${component}:${symbolName}`;
|
|
29
|
-
}
|
|
30
|
-
function settersForComponent(setters, component) {
|
|
31
|
-
if (!component)
|
|
32
|
-
return new Map(setters);
|
|
33
|
-
const scoped = new Map(setters);
|
|
34
|
-
for (const [key, setter] of setters) {
|
|
35
|
-
if (!key.startsWith(`${component}:`))
|
|
36
|
-
continue;
|
|
37
|
-
scoped.set(key.slice(component.length + 1), setter);
|
|
38
|
-
}
|
|
39
|
-
return scoped;
|
|
40
|
-
}
|
|
41
|
-
function discoverContextBindings(source, fileName, route, typeAliases) {
|
|
42
|
-
const bindings = emptyContextBindings();
|
|
43
|
-
const providerValues = new Map();
|
|
44
|
-
const visitProvider = (node, componentName) => {
|
|
45
|
-
const component = componentNameFor(node) ?? componentName;
|
|
46
|
-
const localSetters = new Map();
|
|
47
|
-
if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && component && node.body && ts.isBlock(node.body)) {
|
|
48
|
-
for (const statement of node.body.statements) {
|
|
49
|
-
if (!ts.isVariableStatement(statement))
|
|
50
|
-
continue;
|
|
51
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
52
|
-
if (!ts.isArrayBindingPattern(declaration.name) || !declaration.initializer || !isUseStateCall(declaration.initializer))
|
|
53
|
-
continue;
|
|
54
|
-
const stateName = declaration.name.elements[0];
|
|
55
|
-
const setterName = declaration.name.elements[1];
|
|
56
|
-
if (!setterName || !ts.isBindingElement(stateName) || !ts.isIdentifier(stateName.name) || !ts.isBindingElement(setterName) || !ts.isIdentifier(setterName.name))
|
|
57
|
-
continue;
|
|
58
|
-
const domain = inferUseStateDomain(declaration.initializer, typeAliases);
|
|
59
|
-
const varId = `local:${component}.${stateName.name.text}`;
|
|
60
|
-
const setter = { varId, component, stateName: stateName.name.text, domain };
|
|
61
|
-
localSetters.set(setterName.name.text, setter);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const statement of node.body.statements) {
|
|
65
|
-
if (!ts.isVariableStatement(statement))
|
|
66
|
-
continue;
|
|
67
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
68
|
-
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
69
|
-
continue;
|
|
70
|
-
const setter = setterAliasBinding(declaration.initializer, localSetters);
|
|
71
|
-
if (setter)
|
|
72
|
-
localSetters.set(declaration.name.text, setter);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (component && localSetters.size > 0 && node.getText(source).includes(".Provider")) {
|
|
77
|
-
const fields = providerValueFields(node, localSetters);
|
|
78
|
-
if (fields.size > 0) {
|
|
79
|
-
providerValues.set(component, fields);
|
|
80
|
-
for (const setter of fields.values())
|
|
81
|
-
bindings.setters.set(setter.stateName, setter);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
ts.forEachChild(node, (child) => visitProvider(child, component));
|
|
85
|
-
};
|
|
86
|
-
visitProvider(source, undefined);
|
|
87
|
-
const providerFieldMaps = [...providerValues.values()];
|
|
88
|
-
const visitHook = (node) => {
|
|
89
|
-
const name = customHookDeclarationName(node);
|
|
90
|
-
if (name && (ts.isFunctionDeclaration(node) || (ts.isVariableDeclaration(node) && node.initializer && isExtractableHandler(node.initializer)))) {
|
|
91
|
-
const hook = ts.isFunctionDeclaration(node) ? node : node.initializer;
|
|
92
|
-
if (hookUsesContext(hook) && providerFieldMaps.length > 0) {
|
|
93
|
-
const merged = new Map();
|
|
94
|
-
for (const map of providerFieldMaps)
|
|
95
|
-
for (const [field, setter] of map)
|
|
96
|
-
merged.set(field, setter);
|
|
97
|
-
bindings.hookReturns.set(name, merged);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
ts.forEachChild(node, visitHook);
|
|
101
|
-
};
|
|
102
|
-
visitHook(source);
|
|
103
|
-
return bindings;
|
|
104
|
-
}
|
|
105
|
-
function providerValueFields(node, localSetters) {
|
|
106
|
-
const fields = new Map();
|
|
107
|
-
const visit = (candidate) => {
|
|
108
|
-
if (ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "value" && candidate.initializer && ts.isJsxExpression(candidate.initializer)) {
|
|
109
|
-
const value = providerValueObject(node, candidate.initializer.expression);
|
|
110
|
-
if (value) {
|
|
111
|
-
for (const property of value.properties) {
|
|
112
|
-
if (!ts.isShorthandPropertyAssignment(property) && !ts.isPropertyAssignment(property))
|
|
113
|
-
continue;
|
|
114
|
-
const name = ts.isShorthandPropertyAssignment(property) ? property.name.text : propertyName(property.name);
|
|
115
|
-
const expr = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
116
|
-
if (!name || !ts.isIdentifier(expr))
|
|
117
|
-
continue;
|
|
118
|
-
const setter = localSetters.get(expr.text);
|
|
119
|
-
if (setter)
|
|
120
|
-
fields.set(name, setter);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
ts.forEachChild(candidate, visit);
|
|
125
|
-
};
|
|
126
|
-
visit(node);
|
|
127
|
-
return fields;
|
|
128
|
-
}
|
|
129
|
-
function setterAliasBinding(expression, localSetters) {
|
|
130
|
-
const callback = useCallbackFunction(expression);
|
|
131
|
-
if (!callback || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name))
|
|
132
|
-
return undefined;
|
|
133
|
-
const parameter = callback.parameters[0].name.text;
|
|
134
|
-
const call = firstCallInFunction(callback);
|
|
135
|
-
if (!call || !ts.isIdentifier(call.expression) || call.arguments.length !== 1 || !ts.isIdentifier(call.arguments[0]) || call.arguments[0].text !== parameter)
|
|
136
|
-
return undefined;
|
|
137
|
-
return localSetters.get(call.expression.text);
|
|
138
|
-
}
|
|
139
|
-
function useCallbackFunction(expression) {
|
|
140
|
-
if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.expression.text !== "useCallback")
|
|
141
|
-
return undefined;
|
|
142
|
-
const first = expression.arguments[0];
|
|
143
|
-
return first && isExtractableHandler(first) ? first : undefined;
|
|
144
|
-
}
|
|
145
|
-
function firstCallInFunction(fn) {
|
|
146
|
-
if (!ts.isBlock(fn.body))
|
|
147
|
-
return ts.isCallExpression(fn.body) ? fn.body : undefined;
|
|
148
|
-
for (const statement of fn.body.statements) {
|
|
149
|
-
if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
|
|
150
|
-
return statement.expression;
|
|
151
|
-
}
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
function providerValueObject(scope, expression) {
|
|
155
|
-
if (!expression)
|
|
156
|
-
return undefined;
|
|
157
|
-
if (ts.isObjectLiteralExpression(expression))
|
|
158
|
-
return expression;
|
|
159
|
-
if (!ts.isIdentifier(expression))
|
|
160
|
-
return undefined;
|
|
161
|
-
const declaration = variableDeclarationIn(scope, expression.text);
|
|
162
|
-
if (!declaration?.initializer || !ts.isCallExpression(declaration.initializer))
|
|
163
|
-
return undefined;
|
|
164
|
-
if (!ts.isIdentifier(declaration.initializer.expression) || declaration.initializer.expression.text !== "useMemo")
|
|
165
|
-
return undefined;
|
|
166
|
-
const callback = declaration.initializer.arguments[0];
|
|
167
|
-
if (!callback || !isExtractableHandler(callback))
|
|
168
|
-
return undefined;
|
|
169
|
-
if (ts.isObjectLiteralExpression(callback.body))
|
|
170
|
-
return callback.body;
|
|
171
|
-
if (ts.isParenthesizedExpression(callback.body) && ts.isObjectLiteralExpression(callback.body.expression))
|
|
172
|
-
return callback.body.expression;
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
function variableDeclarationIn(scope, name) {
|
|
176
|
-
let found;
|
|
177
|
-
const visit = (node) => {
|
|
178
|
-
if (found)
|
|
179
|
-
return;
|
|
180
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
|
|
181
|
-
found = node;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
ts.forEachChild(node, visit);
|
|
185
|
-
};
|
|
186
|
-
visit(scope);
|
|
187
|
-
return found;
|
|
188
|
-
}
|
|
189
|
-
function hookUsesContext(hook) {
|
|
190
|
-
let found = false;
|
|
191
|
-
const visit = (node) => {
|
|
192
|
-
if (found)
|
|
193
|
-
return;
|
|
194
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useContext") {
|
|
195
|
-
found = true;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
ts.forEachChild(node, visit);
|
|
199
|
-
};
|
|
200
|
-
visit(hook);
|
|
201
|
-
return found;
|
|
202
|
-
}
|
|
203
|
-
function bindContextHookObjectDeclaration(node, contextBindings, setters) {
|
|
204
|
-
if (!ts.isVariableDeclaration(node) || !ts.isObjectBindingPattern(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression))
|
|
205
|
-
return;
|
|
206
|
-
const hook = contextBindings.hookReturns.get(node.initializer.expression.text);
|
|
207
|
-
if (!hook)
|
|
208
|
-
return;
|
|
209
|
-
for (const element of node.name.elements) {
|
|
210
|
-
if (!ts.isIdentifier(element.name))
|
|
211
|
-
continue;
|
|
212
|
-
const property = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.name.text;
|
|
213
|
-
const setter = hook.get(property);
|
|
214
|
-
if (setter)
|
|
215
|
-
setters.set(element.name.text, setter);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
export function inferDomainFromTypeNode(node, typeAliases = new Map()) {
|
|
219
|
-
if (!node)
|
|
220
|
-
return { kind: "tokens", count: 1 };
|
|
221
|
-
switch (node.kind) {
|
|
222
|
-
case ts.SyntaxKind.BooleanKeyword:
|
|
223
|
-
return { kind: "bool" };
|
|
224
|
-
case ts.SyntaxKind.StringKeyword:
|
|
225
|
-
case ts.SyntaxKind.NumberKeyword:
|
|
226
|
-
case ts.SyntaxKind.AnyKeyword:
|
|
227
|
-
case ts.SyntaxKind.UnknownKeyword:
|
|
228
|
-
return { kind: "tokens", count: 1 };
|
|
229
|
-
case ts.SyntaxKind.LiteralType:
|
|
230
|
-
return domainFromLiteralType(node);
|
|
231
|
-
case ts.SyntaxKind.UnionType:
|
|
232
|
-
return domainFromUnion(node, typeAliases);
|
|
233
|
-
case ts.SyntaxKind.TypeLiteral:
|
|
234
|
-
return domainFromTypeLiteral(node);
|
|
235
|
-
case ts.SyntaxKind.ArrayType:
|
|
236
|
-
return { kind: "lengthCat" };
|
|
237
|
-
case ts.SyntaxKind.TypeReference:
|
|
238
|
-
return domainFromTypeReference(node, typeAliases);
|
|
239
|
-
default:
|
|
240
|
-
return { kind: "tokens", count: 1 };
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
export function extractUseStateVars(sourceText, options = {}) {
|
|
244
|
-
return extractUseStateSkeleton(sourceText, options);
|
|
245
|
-
}
|
|
246
|
-
export function extractUseStateSkeleton(sourceText, options = {}) {
|
|
247
|
-
const fileName = options.fileName ?? "App.tsx";
|
|
248
|
-
const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
249
|
-
const typeAliases = typeAliasDeclarations(source);
|
|
250
|
-
const vars = options.stateVars ? [...options.stateVars] : [];
|
|
251
|
-
const transitions = [];
|
|
252
|
-
const warnings = [];
|
|
253
|
-
const route = options.route ?? "/";
|
|
254
|
-
const routePatterns = options.routePatterns ?? [];
|
|
255
|
-
const effectApis = new Set(options.effectApis ?? []);
|
|
256
|
-
const sourcePlugins = options.sourcePlugins ?? [];
|
|
257
|
-
const routerPlugin = options.routerPlugin;
|
|
258
|
-
const setters = new Map();
|
|
259
|
-
const contextBindings = discoverContextBindings(source, fileName, route, typeAliases);
|
|
260
|
-
const globalTaints = new Set();
|
|
261
|
-
const components = componentDeclarations(source);
|
|
262
|
-
const providerComponents = providerComponentNames(source);
|
|
263
|
-
const customHooks = customHookDeclarations(source);
|
|
264
|
-
const statefulListComponents = detectStatefulListComponents(source, components);
|
|
265
|
-
const reportedStatefulListComponents = new Set();
|
|
266
|
-
const reportedCustomHooks = new Set();
|
|
267
|
-
if (options.stateVars && options.writeChannels) {
|
|
268
|
-
for (const channel of options.writeChannels) {
|
|
269
|
-
const decl = options.stateVars.find((candidate) => candidate.id === channel.varId);
|
|
270
|
-
if (!decl)
|
|
271
|
-
continue;
|
|
272
|
-
bindSetter(setters, channel.symbolName, setterBindingFromDecl(decl));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
for (const decl of contextBindings.vars) {
|
|
276
|
-
if (!vars.some((candidate) => candidate.id === decl.id))
|
|
277
|
-
vars.push(decl);
|
|
278
|
-
}
|
|
279
|
-
for (const [symbolName, setter] of contextBindings.setters)
|
|
280
|
-
setters.set(symbolName, setter);
|
|
281
|
-
const handlers = new Map();
|
|
282
|
-
const visit = (node, componentName) => {
|
|
283
|
-
if (!componentName && isCustomHookDeclaration(node))
|
|
284
|
-
return;
|
|
285
|
-
const nextComponent = componentNameFor(node) ?? componentName;
|
|
286
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
287
|
-
const handler = extractableHandlerInitializer(node.initializer);
|
|
288
|
-
if (handler)
|
|
289
|
-
handlers.set(node.name.text, handler);
|
|
290
|
-
}
|
|
291
|
-
if (ts.isFunctionDeclaration(node) && node.name && isExtractableHandler(node)) {
|
|
292
|
-
handlers.set(node.name.text, node);
|
|
293
|
-
}
|
|
294
|
-
if (ts.isVariableDeclaration(node) && node.initializer && isUseReducerCall(node.initializer)) {
|
|
295
|
-
warnings.push({ message: `Unsupported useReducer ${nextComponent ?? "Anonymous"}.useReducer`, ...lineAndColumn(source, node) });
|
|
296
|
-
}
|
|
297
|
-
bindContextHookObjectDeclaration(node, contextBindings, setters);
|
|
298
|
-
if (ts.isVariableDeclaration(node) && nextComponent && inlineCustomHookState(source, fileName, node, customHooks, vars, setters, nextComponent, route)) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const customHook = calledCustomHook(node, new Set(customHooks.keys()));
|
|
302
|
-
if (customHook && nextComponent) {
|
|
303
|
-
const key = `${nextComponent}.${customHook}`;
|
|
304
|
-
if (!contextBindings.hookReturns.has(customHook) && !reportedCustomHooks.has(key)) {
|
|
305
|
-
reportedCustomHooks.add(key);
|
|
306
|
-
warnings.push({ message: `Unextractable custom hook ${key}`, ...lineAndColumn(source, node) });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && isUseStateCall(node.initializer)) {
|
|
310
|
-
if (nextComponent && statefulListComponents.has(nextComponent)) {
|
|
311
|
-
if (!reportedStatefulListComponents.has(nextComponent)) {
|
|
312
|
-
reportedStatefulListComponents.add(nextComponent);
|
|
313
|
-
warnings.push({ message: `Unextractable stateful list item ${nextComponent}`, ...lineAndColumn(source, node) });
|
|
314
|
-
}
|
|
315
|
-
ts.forEachChild(node, (child) => visit(child, nextComponent));
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
const stateName = node.name.elements[0];
|
|
319
|
-
const setterName = node.name.elements[1];
|
|
320
|
-
if (ts.isBindingElement(stateName) && ts.isIdentifier(stateName.name)) {
|
|
321
|
-
const domain = inferUseStateDomain(node.initializer, typeAliases);
|
|
322
|
-
const component = nextComponent ?? "Anonymous";
|
|
323
|
-
const varId = `local:${component}.${stateName.name.text}`;
|
|
324
|
-
if (!options.stateVars) {
|
|
325
|
-
vars.push({
|
|
326
|
-
id: varId,
|
|
327
|
-
domain,
|
|
328
|
-
origin: { file: fileName, ...lineAndColumn(source, node) },
|
|
329
|
-
scope: providerComponents.has(component) ? { kind: "global" } : { kind: "route-local", route },
|
|
330
|
-
initial: initialValueForUseState(node.initializer, domain)
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
if (setterName && ts.isBindingElement(setterName) && ts.isIdentifier(setterName.name)) {
|
|
334
|
-
if (!options.writeChannels)
|
|
335
|
-
bindSetter(setters, setterName.name.text, { varId, component, stateName: stateName.name.text, domain });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
warnings.push({ message: "Unsupported useState binding pattern", ...lineAndColumn(source, node) });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
const link = linkNavigationTransition(source, fileName, node, nextComponent ?? "Anonymous", routePatterns);
|
|
343
|
-
if (link)
|
|
344
|
-
transitions.push(link);
|
|
345
|
-
const scopedSetters = settersForComponent(setters, nextComponent);
|
|
346
|
-
const refTaint = refSetterTaint(node, scopedSetters);
|
|
347
|
-
if (refTaint) {
|
|
348
|
-
const key = `Global taint ${refTaint.varId}`;
|
|
349
|
-
if (!globalTaints.has(key)) {
|
|
350
|
-
globalTaints.add(key);
|
|
351
|
-
warnings.push({ message: key, ...lineAndColumn(source, refTaint.node) });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
transitions.push(...transitionsFromTimerCall(source, fileName, node, scopedSetters, nextComponent ?? "Anonymous"));
|
|
355
|
-
for (const timerTaint of timerSetterTaints(node, scopedSetters)) {
|
|
356
|
-
const key = `Global taint ${timerTaint.varId}`;
|
|
357
|
-
if (!globalTaints.has(key)) {
|
|
358
|
-
globalTaints.add(key);
|
|
359
|
-
warnings.push({ message: key, ...lineAndColumn(source, timerTaint.node) });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isForwardablePropName(node.name.text) && !isIntrinsicJsxAttribute(node)) {
|
|
363
|
-
const extracted = transitionsFromComponentPropAttribute(source, fileName, node, scopedSetters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, warnings);
|
|
364
|
-
transitions.push(...extracted);
|
|
365
|
-
if (extracted.length === 0) {
|
|
366
|
-
warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isEventAttribute(node.name.text) && isIntrinsicJsxAttribute(node)) {
|
|
370
|
-
const listInfo = listRenderedHandlerInfo(node, vars, nextComponent ?? "Anonymous");
|
|
371
|
-
if (listInfo) {
|
|
372
|
-
if (listInfo.domain.kind === "boundedList") {
|
|
373
|
-
const extracted = transitionsFromBoundedListAttribute(source, fileName, node, scopedSetters, handlers, nextComponent ?? "Anonymous", {
|
|
374
|
-
varId: listInfo.varId,
|
|
375
|
-
domain: listInfo.domain,
|
|
376
|
-
itemName: listInfo.itemName
|
|
377
|
-
});
|
|
378
|
-
if (extracted.length > 0) {
|
|
379
|
-
transitions.push(...tagStableIdKey(extracted, node));
|
|
380
|
-
ts.forEachChild(node, (child) => visit(child, nextComponent));
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
warnings.push({ message: `Unextractable list-rendered handler ${nextComponent ?? "Anonymous"}.${node.name.text} over ${listInfo.domain.kind} ${listInfo.varId}`, ...lineAndColumn(source, node) });
|
|
385
|
-
ts.forEachChild(node, (child) => visit(child, nextComponent));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const guardLocals = componentGuardLocalsFor(node, scopedSetters);
|
|
389
|
-
const guard = combineParsedGuards([
|
|
390
|
-
renderGuardFor(node, scopedSetters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
|
|
391
|
-
disabledGuardFor(node, scopedSetters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
|
|
392
|
-
]);
|
|
393
|
-
const extracted = transitionsFromJsxAttribute(source, fileName, node, scopedSetters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, routePatterns, contextBindings, warnings);
|
|
394
|
-
transitions.push(...extracted);
|
|
395
|
-
if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, scopedSetters)) {
|
|
396
|
-
warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (ts.isCallExpression(node) && isUseEffectCall(node)) {
|
|
400
|
-
const extracted = transitionsFromUseEffect(source, fileName, node, scopedSetters, nextComponent ?? "Anonymous");
|
|
401
|
-
transitions.push(...extracted);
|
|
402
|
-
if (extracted.length === 0 && useEffectWritesModeledState(node, scopedSetters) && !providerComponents.has(nextComponent ?? "")) {
|
|
403
|
-
warnings.push({ message: `Unextractable effect ${nextComponent ?? "Anonymous"}.useEffect`, ...lineAndColumn(source, node) });
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
ts.forEachChild(node, (child) => visit(child, nextComponent));
|
|
407
|
-
};
|
|
408
|
-
visit(source, undefined);
|
|
409
|
-
return { vars, transitions: withStableTransitionIds(transitions), warnings };
|
|
410
|
-
}
|
|
411
|
-
function withStableTransitionIds(transitions) {
|
|
412
|
-
const groups = new Map();
|
|
413
|
-
for (const transition of transitions) {
|
|
414
|
-
const group = groups.get(transition.id) ?? [];
|
|
415
|
-
group.push(transition);
|
|
416
|
-
groups.set(transition.id, group);
|
|
417
|
-
}
|
|
418
|
-
const emitted = new Map();
|
|
419
|
-
return transitions.map((transition) => {
|
|
420
|
-
const internal = transition;
|
|
421
|
-
const group = groups.get(transition.id) ?? [];
|
|
422
|
-
const base = stripInternalTransition(internal);
|
|
423
|
-
if (group.length <= 1)
|
|
424
|
-
return base;
|
|
425
|
-
const suffix = shortHash(internal.__stableIdKey ?? canonicalTransitionKey(base));
|
|
426
|
-
const id = `${transition.id}.${suffix}`;
|
|
427
|
-
const count = emitted.get(id) ?? 0;
|
|
428
|
-
emitted.set(id, count + 1);
|
|
429
|
-
return { ...base, id: count === 0 ? id : `${id}.${count + 1}` };
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
function stripInternalTransition(transition) {
|
|
433
|
-
const { __stableIdKey: _ignored, ...publicTransition } = transition;
|
|
434
|
-
return publicTransition;
|
|
435
|
-
}
|
|
436
|
-
function canonicalTransitionKey(transition) {
|
|
437
|
-
return JSON.stringify({
|
|
438
|
-
label: transition.label,
|
|
439
|
-
guard: transition.guard,
|
|
440
|
-
effect: transition.effect,
|
|
441
|
-
reads: transition.reads,
|
|
442
|
-
writes: transition.writes
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
function shortHash(value) {
|
|
446
|
-
let hash = 0x811c9dc5;
|
|
447
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
448
|
-
hash ^= value.charCodeAt(i);
|
|
449
|
-
hash = Math.imul(hash, 0x01000193);
|
|
450
|
-
}
|
|
451
|
-
return (hash >>> 0).toString(36).padStart(6, "0").slice(0, 6);
|
|
452
|
-
}
|
|
453
|
-
function inferUseStateDomain(call, typeAliases = new Map()) {
|
|
454
|
-
const typeArg = call.typeArguments?.[0];
|
|
455
|
-
if (typeArg)
|
|
456
|
-
return inferDomainFromTypeNode(typeArg, typeAliases);
|
|
457
|
-
const initial = call.arguments[0];
|
|
458
|
-
if (!initial)
|
|
459
|
-
return { kind: "tokens", count: 1 };
|
|
460
|
-
if (initial.kind === ts.SyntaxKind.TrueKeyword || initial.kind === ts.SyntaxKind.FalseKeyword)
|
|
461
|
-
return { kind: "bool" };
|
|
462
|
-
if (ts.isStringLiteral(initial) || ts.isNumericLiteral(initial))
|
|
463
|
-
return { kind: "enum", values: [initial.text] };
|
|
464
|
-
if (initial.kind === ts.SyntaxKind.NullKeyword)
|
|
465
|
-
return { kind: "option", inner: { kind: "tokens", count: 1 } };
|
|
466
|
-
if (ts.isArrayLiteralExpression(initial))
|
|
467
|
-
return { kind: "lengthCat" };
|
|
468
|
-
return { kind: "tokens", count: 1 };
|
|
469
|
-
}
|
|
470
|
-
function initialValueForUseState(call, domain) {
|
|
471
|
-
const initial = call.arguments[0];
|
|
472
|
-
if (!initial)
|
|
473
|
-
return firstValue(domain);
|
|
474
|
-
const parsed = initialValueFromExpression(initial, domain);
|
|
475
|
-
if (parsed !== undefined)
|
|
476
|
-
return parsed;
|
|
477
|
-
if (initial.kind === ts.SyntaxKind.TrueKeyword)
|
|
478
|
-
return true;
|
|
479
|
-
if (initial.kind === ts.SyntaxKind.FalseKeyword)
|
|
480
|
-
return false;
|
|
481
|
-
if (ts.isStringLiteral(initial))
|
|
482
|
-
return initial.text;
|
|
483
|
-
if (ts.isNumericLiteral(initial))
|
|
484
|
-
return Number(initial.text);
|
|
485
|
-
if (initial.kind === ts.SyntaxKind.NullKeyword)
|
|
486
|
-
return null;
|
|
487
|
-
if (ts.isArrayLiteralExpression(initial))
|
|
488
|
-
return initial.elements.length === 0 ? "0" : initial.elements.length === 1 ? "1" : "many";
|
|
489
|
-
return firstValue(domain);
|
|
490
|
-
}
|
|
491
|
-
function initialValueFromExpression(expression, domain) {
|
|
492
|
-
const literal = literalValue(expression);
|
|
493
|
-
if (literal !== undefined)
|
|
494
|
-
return literal;
|
|
495
|
-
if (domain.kind === "option")
|
|
496
|
-
return initialValueFromExpression(expression, domain.inner);
|
|
497
|
-
if (domain.kind === "record" && ts.isObjectLiteralExpression(expression)) {
|
|
498
|
-
const fields = {};
|
|
499
|
-
for (const [field, fieldDomain] of Object.entries(domain.fields)) {
|
|
500
|
-
const property = expression.properties.find((candidate) => ts.isPropertyAssignment(candidate) && propertyName(candidate.name) === field);
|
|
501
|
-
fields[field] = property ? initialValueFromExpression(property.initializer, fieldDomain) ?? firstValue(fieldDomain) : firstValue(fieldDomain);
|
|
502
|
-
}
|
|
503
|
-
return fields;
|
|
504
|
-
}
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
function domainFromLiteralType(node) {
|
|
508
|
-
const lit = node.literal;
|
|
509
|
-
if (lit.kind === ts.SyntaxKind.TrueKeyword || lit.kind === ts.SyntaxKind.FalseKeyword)
|
|
510
|
-
return { kind: "bool" };
|
|
511
|
-
if (ts.isStringLiteral(lit))
|
|
512
|
-
return { kind: "enum", values: [lit.text] };
|
|
513
|
-
if (ts.isNumericLiteral(lit))
|
|
514
|
-
return { kind: "boundedInt", min: Number(lit.text), max: Number(lit.text) };
|
|
515
|
-
if (lit.kind === ts.SyntaxKind.NullKeyword)
|
|
516
|
-
return { kind: "option", inner: { kind: "tokens", count: 1 } };
|
|
517
|
-
return { kind: "tokens", count: 1 };
|
|
518
|
-
}
|
|
519
|
-
function domainFromUnion(node, typeAliases = new Map()) {
|
|
520
|
-
const nonNull = node.types.filter((part) => part.kind !== ts.SyntaxKind.UndefinedKeyword && !(ts.isLiteralTypeNode(part) && part.literal.kind === ts.SyntaxKind.NullKeyword));
|
|
521
|
-
if (nonNull.length !== node.types.length && nonNull.length > 0) {
|
|
522
|
-
return { kind: "option", inner: nonNull.length === 1 ? inferDomainFromTypeNode(nonNull[0], typeAliases) : domainFromUnionMembers(nonNull) };
|
|
523
|
-
}
|
|
524
|
-
return domainFromUnionMembers(node.types);
|
|
525
|
-
}
|
|
526
|
-
function domainFromUnionMembers(types) {
|
|
527
|
-
const literalValues = [];
|
|
528
|
-
const numericValues = [];
|
|
529
|
-
for (const part of types) {
|
|
530
|
-
if (!ts.isLiteralTypeNode(part))
|
|
531
|
-
return taggedUnionFromMembers(types) ?? { kind: "tokens", count: 1 };
|
|
532
|
-
const lit = part.literal;
|
|
533
|
-
if (ts.isStringLiteral(lit))
|
|
534
|
-
literalValues.push(lit.text);
|
|
535
|
-
else if (ts.isNumericLiteral(lit))
|
|
536
|
-
numericValues.push(Number(lit.text));
|
|
537
|
-
else
|
|
538
|
-
return taggedUnionFromMembers(types) ?? { kind: "tokens", count: 1 };
|
|
539
|
-
}
|
|
540
|
-
if (numericValues.length === types.length) {
|
|
541
|
-
return { kind: "boundedInt", min: Math.min(...numericValues), max: Math.max(...numericValues) };
|
|
542
|
-
}
|
|
543
|
-
return { kind: "enum", values: literalValues };
|
|
544
|
-
}
|
|
545
|
-
function taggedUnionFrom(node) {
|
|
546
|
-
return taggedUnionFromMembers(node.types);
|
|
547
|
-
}
|
|
548
|
-
function taggedUnionFromMembers(types) {
|
|
549
|
-
const members = types.filter(ts.isTypeLiteralNode);
|
|
550
|
-
if (members.length !== types.length)
|
|
551
|
-
return undefined;
|
|
552
|
-
const tagCandidates = new Map();
|
|
553
|
-
for (const member of members) {
|
|
554
|
-
for (const prop of member.members.filter(ts.isPropertySignature)) {
|
|
555
|
-
if (!prop.type || !ts.isIdentifier(prop.name) || !ts.isLiteralTypeNode(prop.type) || !ts.isStringLiteral(prop.type.literal))
|
|
556
|
-
continue;
|
|
557
|
-
const set = tagCandidates.get(prop.name.text) ?? new Set();
|
|
558
|
-
set.add(prop.type.literal.text);
|
|
559
|
-
tagCandidates.set(prop.name.text, set);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const tag = [...tagCandidates].find(([, values]) => values.size === members.length)?.[0];
|
|
563
|
-
if (!tag)
|
|
564
|
-
return undefined;
|
|
565
|
-
const variants = {};
|
|
566
|
-
for (const member of members) {
|
|
567
|
-
const tagProp = member.members.find((prop) => ts.isPropertySignature(prop) && ts.isIdentifier(prop.name) && prop.name.text === tag);
|
|
568
|
-
if (!tagProp?.type || !ts.isLiteralTypeNode(tagProp.type) || !ts.isStringLiteral(tagProp.type.literal))
|
|
569
|
-
return undefined;
|
|
570
|
-
variants[tagProp.type.literal.text] = domainFromTypeLiteral(member, tag);
|
|
571
|
-
}
|
|
572
|
-
return { kind: "tagged", tag, variants };
|
|
573
|
-
}
|
|
574
|
-
function domainFromTypeLiteral(node, omitField) {
|
|
575
|
-
const fields = {};
|
|
576
|
-
for (const member of node.members) {
|
|
577
|
-
if (!ts.isPropertySignature(member) || !member.type || !ts.isIdentifier(member.name) || member.name.text === omitField)
|
|
578
|
-
continue;
|
|
579
|
-
fields[member.name.text] = inferDomainFromTypeNode(member.type);
|
|
580
|
-
}
|
|
581
|
-
return { kind: "record", fields };
|
|
582
|
-
}
|
|
583
|
-
function domainFromTypeReference(node, typeAliases = new Map()) {
|
|
584
|
-
const name = node.typeName.getText();
|
|
585
|
-
const alias = typeAliases.get(name);
|
|
586
|
-
if (alias)
|
|
587
|
-
return inferDomainFromTypeNode(alias, typeAliases);
|
|
588
|
-
if ((name === "Array" || name === "ReadonlyArray") && node.typeArguments?.length === 1)
|
|
589
|
-
return { kind: "lengthCat" };
|
|
590
|
-
if (name === "Record")
|
|
591
|
-
return { kind: "tokens", count: 1 };
|
|
592
|
-
return { kind: "tokens", count: 1 };
|
|
593
|
-
}
|
|
594
|
-
function typeAliasDeclarations(source) {
|
|
595
|
-
const aliases = new Map();
|
|
596
|
-
const visit = (node) => {
|
|
597
|
-
if (ts.isTypeAliasDeclaration(node) && ts.isIdentifier(node.name))
|
|
598
|
-
aliases.set(node.name.text, node.type);
|
|
599
|
-
ts.forEachChild(node, visit);
|
|
600
|
-
};
|
|
601
|
-
visit(source);
|
|
602
|
-
return aliases;
|
|
603
|
-
}
|
|
604
|
-
function isUseStateCall(node) {
|
|
605
|
-
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useState";
|
|
606
|
-
}
|
|
607
|
-
function isUseReducerCall(node) {
|
|
608
|
-
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useReducer";
|
|
609
|
-
}
|
|
610
|
-
function isUseRefCall(node) {
|
|
611
|
-
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useRef";
|
|
612
|
-
}
|
|
613
|
-
function isUseEffectCall(node) {
|
|
614
|
-
return ts.isIdentifier(node.expression) && node.expression.text === "useEffect";
|
|
615
|
-
}
|
|
616
|
-
function isExtractableHandler(node) {
|
|
617
|
-
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || (ts.isFunctionDeclaration(node) && Boolean(node.body));
|
|
618
|
-
}
|
|
619
|
-
function extractableHandlerInitializer(node) {
|
|
620
|
-
if (isExtractableHandler(node))
|
|
621
|
-
return node;
|
|
622
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useCallback") {
|
|
623
|
-
const callback = node.arguments[0];
|
|
624
|
-
return callback && isExtractableHandler(callback) ? callback : undefined;
|
|
625
|
-
}
|
|
626
|
-
return undefined;
|
|
627
|
-
}
|
|
628
|
-
function refSetterTaint(node, setters) {
|
|
629
|
-
if (ts.isVariableDeclaration(node) && node.initializer && isUseRefCall(node.initializer)) {
|
|
630
|
-
const arg = node.initializer.arguments[0];
|
|
631
|
-
if (arg && ts.isIdentifier(arg)) {
|
|
632
|
-
const setter = setters.get(arg.text);
|
|
633
|
-
if (setter)
|
|
634
|
-
return { varId: setter.varId, node: arg };
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "current" && ts.isIdentifier(node.right)) {
|
|
638
|
-
const setter = setters.get(node.right.text);
|
|
639
|
-
if (setter)
|
|
640
|
-
return { varId: setter.varId, node: node.right };
|
|
641
|
-
}
|
|
642
|
-
return undefined;
|
|
643
|
-
}
|
|
644
|
-
function timerSetterTaints(node, setters) {
|
|
645
|
-
if (!ts.isCallExpression(node))
|
|
646
|
-
return [];
|
|
647
|
-
const name = callName(node.expression);
|
|
648
|
-
if (name !== "setTimeout" && name !== "setInterval")
|
|
649
|
-
return [];
|
|
650
|
-
const callback = node.arguments[0];
|
|
651
|
-
if (!callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)))
|
|
652
|
-
return [];
|
|
653
|
-
if (timerCallbackSummaries(callback, setters))
|
|
654
|
-
return [];
|
|
655
|
-
return uniqueSetters(settersWrittenIn(callback.body, setters)).map((setter) => ({ varId: setter.varId, node: callback }));
|
|
656
|
-
}
|
|
657
|
-
function transitionsFromTimerCall(source, fileName, node, setters, component) {
|
|
658
|
-
if (!ts.isCallExpression(node))
|
|
659
|
-
return [];
|
|
660
|
-
const name = callName(node.expression);
|
|
661
|
-
if (name !== "setTimeout" && name !== "setInterval")
|
|
662
|
-
return [];
|
|
663
|
-
const callback = node.arguments[0];
|
|
664
|
-
if (!callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)))
|
|
665
|
-
return [];
|
|
666
|
-
const summaries = timerCallbackSummaries(callback, setters);
|
|
667
|
-
if (!summaries || summaries.length === 0)
|
|
668
|
-
return [];
|
|
669
|
-
const effects = summaries.map((summary) => summary.effect);
|
|
670
|
-
const writes = uniqueStrings(effects.flatMap(effectWriteVars));
|
|
671
|
-
const suffix = writes.map((id) => stateNameForVar(id, setters) ?? safeId(id)).join("_") || "callback";
|
|
672
|
-
return [{
|
|
673
|
-
id: `${component}.${name}.${suffix}`,
|
|
674
|
-
cls: "env",
|
|
675
|
-
label: { kind: "timer", key: `${component}.${name}.${suffix}` },
|
|
676
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
677
|
-
guard: { kind: "lit", value: true },
|
|
678
|
-
effect: effects.length === 1 ? effects[0] : { kind: "seq", effects },
|
|
679
|
-
reads: uniqueStrings(summaries.flatMap((summary) => summary.reads)),
|
|
680
|
-
writes,
|
|
681
|
-
confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact"
|
|
682
|
-
}];
|
|
683
|
-
}
|
|
684
|
-
function timerCallbackSummaries(callback, setters) {
|
|
685
|
-
if (ts.isCallExpression(callback.body)) {
|
|
686
|
-
const summary = summarizeSetterCall(callback.body, setters);
|
|
687
|
-
return summary ? [summary] : undefined;
|
|
688
|
-
}
|
|
689
|
-
if (!ts.isBlock(callback.body) || callback.body.statements.length === 0)
|
|
690
|
-
return undefined;
|
|
691
|
-
const summaries = [];
|
|
692
|
-
for (const statement of callback.body.statements) {
|
|
693
|
-
const summary = summarizeSetterStatement(statement, setters);
|
|
694
|
-
if (!summary)
|
|
695
|
-
return undefined;
|
|
696
|
-
summaries.push(summary);
|
|
697
|
-
}
|
|
698
|
-
return summaries;
|
|
699
|
-
}
|
|
700
|
-
function handlerSchedulesModeledTimer(attribute, handlers, setters) {
|
|
701
|
-
if (!attribute.initializer)
|
|
702
|
-
return false;
|
|
703
|
-
const expression = ts.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : undefined;
|
|
704
|
-
const handler = handlerExpression(expression, handlers);
|
|
705
|
-
if (!handler)
|
|
706
|
-
return false;
|
|
707
|
-
let found = false;
|
|
708
|
-
const visit = (node) => {
|
|
709
|
-
if (found)
|
|
710
|
-
return;
|
|
711
|
-
if (ts.isCallExpression(node)) {
|
|
712
|
-
const name = callName(node.expression);
|
|
713
|
-
const callback = node.arguments[0];
|
|
714
|
-
if ((name === "setTimeout" || name === "setInterval") &&
|
|
715
|
-
callback &&
|
|
716
|
-
(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) &&
|
|
717
|
-
timerCallbackSummaries(callback, setters)) {
|
|
718
|
-
found = true;
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
ts.forEachChild(node, visit);
|
|
723
|
-
};
|
|
724
|
-
visit(handler.body);
|
|
725
|
-
return found;
|
|
726
|
-
}
|
|
727
|
-
function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, routePatterns, contextBindings, warnings) {
|
|
728
|
-
if (!node.initializer)
|
|
729
|
-
return [];
|
|
730
|
-
const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
|
|
731
|
-
const handler = handlerExpression(expression, handlers);
|
|
732
|
-
if (!handler)
|
|
733
|
-
return [];
|
|
734
|
-
if (!ts.isIdentifier(node.name))
|
|
735
|
-
return [];
|
|
736
|
-
const attr = node.name.text;
|
|
737
|
-
const locator = locatorForEventAttribute(node);
|
|
738
|
-
return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, routePatterns, contextBindings, warnings), handler);
|
|
739
|
-
}
|
|
740
|
-
function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, warnings) {
|
|
741
|
-
if (!node.initializer || !ts.isIdentifier(node.name))
|
|
742
|
-
return [];
|
|
743
|
-
const tag = jsxTagName(node);
|
|
744
|
-
if (!tag)
|
|
745
|
-
return [];
|
|
746
|
-
const callee = components.get(tag);
|
|
747
|
-
if (!callee)
|
|
748
|
-
return [];
|
|
749
|
-
const trigger = componentPropTrigger(source, callee, node.name.text, setters, warnings) ?? transparentComponentPropTrigger(callee, node.name.text);
|
|
750
|
-
if (!trigger)
|
|
751
|
-
return [];
|
|
752
|
-
const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
|
|
753
|
-
const handler = handlerExpression(expression, handlers);
|
|
754
|
-
if (!handler)
|
|
755
|
-
return [];
|
|
756
|
-
const guardLocals = componentGuardLocalsFor(node, setters);
|
|
757
|
-
const callerGuard = combineParsedGuards([
|
|
758
|
-
renderGuardFor(node, setters, warnings, source, component, guardLocals),
|
|
759
|
-
disabledGuardFor(node, setters, warnings, source, component, guardLocals)
|
|
760
|
-
]);
|
|
761
|
-
return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, [], emptyContextBindings(), warnings), handler);
|
|
762
|
-
}
|
|
763
|
-
function transitionsFromBoundedListAttribute(source, fileName, node, setters, handlers, component, listInfo) {
|
|
764
|
-
if (!node.initializer || !ts.isIdentifier(node.name))
|
|
765
|
-
return [];
|
|
766
|
-
const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
|
|
767
|
-
const handler = handlerExpression(expression, handlers);
|
|
768
|
-
if (!handler)
|
|
769
|
-
return [];
|
|
770
|
-
const attr = node.name.text;
|
|
771
|
-
const summary = callSummaryFromHandler(handler, setters, new Map([[listInfo.itemName, readListItemBinding(listInfo.varId, 0)]]));
|
|
772
|
-
if (!summary)
|
|
773
|
-
return [];
|
|
774
|
-
const setterCall = setterCallFrom(summary.call, setters);
|
|
775
|
-
if (!setterCall)
|
|
776
|
-
return [];
|
|
777
|
-
const baseLocator = locatorForEventAttribute(node);
|
|
778
|
-
const transitions = [];
|
|
779
|
-
for (let index = 0; index < listInfo.domain.maxLen; index += 1) {
|
|
780
|
-
const locals = new Map(summary.locals);
|
|
781
|
-
locals.set(listInfo.itemName, readListItemBinding(listInfo.varId, index));
|
|
782
|
-
const assigned = setterArgumentExpr(setterCall.argument, setterCall.setter, setters, locals);
|
|
783
|
-
if (!assigned)
|
|
784
|
-
return [];
|
|
785
|
-
const locator = baseLocator ? { kind: "positional", base: baseLocator, index } : undefined;
|
|
786
|
-
const guard = boundedListIndexGuard(listInfo.varId, index);
|
|
787
|
-
transitions.push({
|
|
788
|
-
id: `${component}.${attr}.${setterCall.setter.stateName}.${index}`,
|
|
789
|
-
cls: "user",
|
|
790
|
-
label: labelForEvent(attr, locator),
|
|
791
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
792
|
-
guard,
|
|
793
|
-
effect: { kind: "assign", var: setterCall.setter.varId, expr: assigned.expr },
|
|
794
|
-
reads: uniqueStrings([listInfo.varId, ...assigned.reads]),
|
|
795
|
-
writes: [setterCall.setter.varId],
|
|
796
|
-
confidence: index <= 1 ? "exact" : "over-approx"
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
return transitions;
|
|
800
|
-
}
|
|
801
|
-
function readListItemBinding(varId, index) {
|
|
802
|
-
return { expr: { kind: "read", var: varId, path: [String(index)] }, reads: [varId] };
|
|
803
|
-
}
|
|
804
|
-
function boundedListIndexGuard(varId, index) {
|
|
805
|
-
const len = { kind: "lenCat", arg: { kind: "read", var: varId } };
|
|
806
|
-
if (index === 0)
|
|
807
|
-
return { kind: "neq", args: [len, { kind: "lit", value: "0" }] };
|
|
808
|
-
return { kind: "eq", args: [len, { kind: "lit", value: "many" }] };
|
|
809
|
-
}
|
|
810
|
-
function tagStableIdKey(transitions, node) {
|
|
811
|
-
const key = normalizedAstKey(node);
|
|
812
|
-
return transitions.map((transition) => ({ ...transition, __stableIdKey: key }));
|
|
813
|
-
}
|
|
814
|
-
function normalizedAstKey(node) {
|
|
815
|
-
return node
|
|
816
|
-
.getText()
|
|
817
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
818
|
-
.replace(/\/\/.*$/gm, "")
|
|
819
|
-
.replace(/\s+/g, "");
|
|
820
|
-
}
|
|
821
|
-
function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, routePatterns, contextBindings, warnings) {
|
|
822
|
-
const asyncTransitions = transitionsFromAsyncHandler(source, fileName, attr, handler, setters, component, effectApis, asyncOutcomes, locator, routePatterns, warnings);
|
|
823
|
-
if (asyncTransitions.length > 0)
|
|
824
|
-
return applyParsedGuard(asyncTransitions, disabledGuard);
|
|
825
|
-
const conditionalTransition = conditionalTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator);
|
|
826
|
-
if (conditionalTransition)
|
|
827
|
-
return applyParsedGuard([conditionalTransition], disabledGuard);
|
|
828
|
-
const loopTransitions = loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator);
|
|
829
|
-
if (loopTransitions.length > 0)
|
|
830
|
-
return applyParsedGuard(loopTransitions, disabledGuard);
|
|
831
|
-
const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator);
|
|
832
|
-
if (sequentialTransition)
|
|
833
|
-
return applyParsedGuard([sequentialTransition], disabledGuard);
|
|
834
|
-
const summary = callSummaryFromHandler(handler, setters, componentScopeLocalsFor(node, setters, contextBindings));
|
|
835
|
-
if (!summary)
|
|
836
|
-
return [];
|
|
837
|
-
const inlined = inlinedHelperCall(summary.call, handlers, setters);
|
|
838
|
-
const inlinedCall = inlined?.call ?? summary.call;
|
|
839
|
-
const locals = inlined?.locals ?? summary.locals;
|
|
840
|
-
const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator, routerPlugin, routePatterns);
|
|
841
|
-
if (navigation)
|
|
842
|
-
return applyParsedGuard([navigation], disabledGuard);
|
|
843
|
-
const pluginWrite = pluginWriteTransition(source, fileName, node, attr, component, inlinedCall, setters, locals, sourcePlugins, locator);
|
|
844
|
-
if (pluginWrite)
|
|
845
|
-
return applyParsedGuard([pluginWrite], disabledGuard);
|
|
846
|
-
const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
|
|
847
|
-
if (swrMutate)
|
|
848
|
-
return applyParsedGuard([swrMutate], disabledGuard);
|
|
849
|
-
const noop = noopCallTransition(source, fileName, node, attr, component, inlinedCall, locator);
|
|
850
|
-
if (noop)
|
|
851
|
-
return applyParsedGuard([noop], disabledGuard);
|
|
852
|
-
const setterCall = setterCallFrom(inlinedCall, setters);
|
|
853
|
-
if (!setterCall) {
|
|
854
|
-
const escaped = escapedSetters(inlinedCall, setters, locals);
|
|
855
|
-
if (escaped.length === 0)
|
|
856
|
-
return [];
|
|
857
|
-
return applyParsedGuard(escapedSetterTransitions(source, fileName, node, attr, component, escaped, locator), disabledGuard);
|
|
858
|
-
}
|
|
859
|
-
const { setter, argument } = setterCall;
|
|
860
|
-
if ((attr === "onChange" || attr === "onInput") && isInputValueExpression(inlinedCall.arguments[0], handler.parameters[0])) {
|
|
861
|
-
return applyParsedGuard(inputTransitions(source, fileName, node, attr, component, setter, locator), disabledGuard);
|
|
862
|
-
}
|
|
863
|
-
const assignment = setterArgumentExpr(argument, setter, setters, locals);
|
|
864
|
-
if (!assignment) {
|
|
865
|
-
return applyParsedGuard([havocSetterTransition(source, fileName, node, attr, component, setter, locator, "unrepresentable")], disabledGuard);
|
|
866
|
-
}
|
|
867
|
-
return applyParsedGuard([{
|
|
868
|
-
id: `${component}.${attr}.${setter.stateName}`,
|
|
869
|
-
cls: "user",
|
|
870
|
-
label: labelForEvent(attr, locator),
|
|
871
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
872
|
-
guard: { kind: "lit", value: true },
|
|
873
|
-
effect: { kind: "assign", var: setter.varId, expr: assignment.expr },
|
|
874
|
-
reads: assignment.reads,
|
|
875
|
-
writes: [setter.varId],
|
|
876
|
-
confidence: "exact"
|
|
877
|
-
}], disabledGuard);
|
|
878
|
-
}
|
|
879
|
-
function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator) {
|
|
880
|
-
if (!ts.isBlock(handler.body))
|
|
881
|
-
return undefined;
|
|
882
|
-
const locals = new Map();
|
|
883
|
-
const summaries = [];
|
|
884
|
-
for (const statement of handler.body.statements) {
|
|
885
|
-
if (bindConstStatement(statement, setters, locals))
|
|
886
|
-
continue;
|
|
887
|
-
const helper = helperSummariesFromStatement(statement, handlers, setters);
|
|
888
|
-
if (helper) {
|
|
889
|
-
summaries.push(...helper);
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
const summary = summarizeSetterStatement(statement, setters, locals);
|
|
893
|
-
if (!summary)
|
|
894
|
-
return undefined;
|
|
895
|
-
summaries.push(summary);
|
|
896
|
-
}
|
|
897
|
-
if (summaries.length <= 1)
|
|
898
|
-
return undefined;
|
|
899
|
-
const effects = summaries.map((summary) => summary.effect);
|
|
900
|
-
const writes = uniqueStrings(effects.flatMap(effectWriteVars));
|
|
901
|
-
return {
|
|
902
|
-
id: `${component}.${attr}.${writes.map((id) => stateNameForVar(id, setters) ?? safeId(id)).join("_")}.seq`,
|
|
903
|
-
cls: "user",
|
|
904
|
-
label: labelForEvent(attr, locator),
|
|
905
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
906
|
-
guard: { kind: "lit", value: true },
|
|
907
|
-
effect: { kind: "seq", effects },
|
|
908
|
-
reads: uniqueStrings(summaries.flatMap((summary) => summary.reads)),
|
|
909
|
-
writes,
|
|
910
|
-
confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact"
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function helperSummariesFromStatement(statement, handlers, setters) {
|
|
914
|
-
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression))
|
|
915
|
-
return undefined;
|
|
916
|
-
const helper = handlers.get(statement.expression.expression.text);
|
|
917
|
-
if (!helper || !ts.isBlock(helper.body))
|
|
918
|
-
return undefined;
|
|
919
|
-
const locals = new Map();
|
|
920
|
-
const summaries = [];
|
|
921
|
-
for (const child of helper.body.statements) {
|
|
922
|
-
if (bindConstStatement(child, setters, locals))
|
|
923
|
-
continue;
|
|
924
|
-
const summary = summarizeSetterStatement(child, setters, locals);
|
|
925
|
-
if (summary)
|
|
926
|
-
summaries.push(summary);
|
|
927
|
-
}
|
|
928
|
-
return summaries.length > 0 ? summaries : undefined;
|
|
929
|
-
}
|
|
930
|
-
function loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator) {
|
|
931
|
-
if (!ts.isBlock(handler.body))
|
|
932
|
-
return [];
|
|
933
|
-
const loopSetters = [];
|
|
934
|
-
for (const statement of handler.body.statements) {
|
|
935
|
-
if (!isLoopStatement(statement))
|
|
936
|
-
continue;
|
|
937
|
-
for (const setter of settersWrittenIn(statement, setters))
|
|
938
|
-
loopSetters.push(setter);
|
|
939
|
-
}
|
|
940
|
-
return uniqueSetters(loopSetters).map((setter) => havocSetterTransition(source, fileName, node, attr, component, setter, locator, "loop"));
|
|
941
|
-
}
|
|
942
|
-
function isLoopStatement(statement) {
|
|
943
|
-
return ts.isForStatement(statement) ||
|
|
944
|
-
ts.isForInStatement(statement) ||
|
|
945
|
-
ts.isForOfStatement(statement) ||
|
|
946
|
-
ts.isWhileStatement(statement) ||
|
|
947
|
-
ts.isDoStatement(statement);
|
|
948
|
-
}
|
|
949
|
-
function settersWrittenIn(node, setters) {
|
|
950
|
-
const found = [];
|
|
951
|
-
const visit = (candidate) => {
|
|
952
|
-
if (ts.isCallExpression(candidate)) {
|
|
953
|
-
const setterCall = setterCallFrom(candidate, setters);
|
|
954
|
-
if (setterCall)
|
|
955
|
-
found.push(setterCall.setter);
|
|
956
|
-
}
|
|
957
|
-
ts.forEachChild(candidate, visit);
|
|
958
|
-
};
|
|
959
|
-
visit(node);
|
|
960
|
-
return found;
|
|
961
|
-
}
|
|
962
|
-
function uniqueSetters(setters) {
|
|
963
|
-
const byVar = new Map();
|
|
964
|
-
for (const setter of setters)
|
|
965
|
-
byVar.set(setter.varId, setter);
|
|
966
|
-
return [...byVar.values()].sort((left, right) => left.varId.localeCompare(right.varId));
|
|
967
|
-
}
|
|
968
|
-
function setterCallFrom(call, setters) {
|
|
969
|
-
if (ts.isIdentifier(call.expression) && call.arguments.length === 1) {
|
|
970
|
-
const setter = setters.get(call.expression.text);
|
|
971
|
-
return setter ? { setter, argument: call.arguments[0] } : undefined;
|
|
972
|
-
}
|
|
973
|
-
const name = callName(call.expression);
|
|
974
|
-
const atomArg = call.arguments[0];
|
|
975
|
-
if (name && call.arguments.length === 2 && atomArg && ts.isIdentifier(atomArg)) {
|
|
976
|
-
const setter = setters.get(`${name}:${atomArg.text}`);
|
|
977
|
-
return setter ? { setter, argument: call.arguments[1] } : undefined;
|
|
978
|
-
}
|
|
979
|
-
return undefined;
|
|
980
|
-
}
|
|
981
|
-
function havocSetterTransition(source, fileName, node, attr, component, setter, locator, suffix) {
|
|
982
|
-
return {
|
|
983
|
-
id: `${component}.${attr}.${setter.stateName}.${suffix}`,
|
|
984
|
-
cls: "user",
|
|
985
|
-
label: labelForEvent(attr, locator),
|
|
986
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
987
|
-
guard: { kind: "lit", value: true },
|
|
988
|
-
effect: { kind: "havoc", var: setter.varId },
|
|
989
|
-
reads: [],
|
|
990
|
-
writes: [setter.varId],
|
|
991
|
-
confidence: "over-approx"
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
function setterArgumentExpr(argument, setter, setters, locals) {
|
|
995
|
-
if (ts.isObjectLiteralExpression(argument)) {
|
|
996
|
-
const object = objectLiteralAssignmentExpr(argument, setter.domain, setters, locals);
|
|
997
|
-
if (object)
|
|
998
|
-
return object;
|
|
999
|
-
}
|
|
1000
|
-
if ((ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && argument.parameters.length === 1 && ts.isIdentifier(argument.parameters[0].name)) {
|
|
1001
|
-
if (ts.isBlock(argument.body))
|
|
1002
|
-
return undefined;
|
|
1003
|
-
return valueExpr(argument.body, setters, new Map([...locals, [argument.parameters[0].name.text, readBinding(setter.varId)]]));
|
|
1004
|
-
}
|
|
1005
|
-
return valueExpr(argument, setters, locals);
|
|
1006
|
-
}
|
|
1007
|
-
function objectLiteralAssignmentExpr(expression, domain, setters, locals) {
|
|
1008
|
-
const value = {};
|
|
1009
|
-
const reads = new Set();
|
|
1010
|
-
const fields = domain.kind === "record" ? domain.fields : domain.kind === "tagged" ? taggedFieldsForObject(expression, domain) : {};
|
|
1011
|
-
for (const property of expression.properties) {
|
|
1012
|
-
if (!ts.isPropertyAssignment(property))
|
|
1013
|
-
return undefined;
|
|
1014
|
-
const name = propertyName(property.name);
|
|
1015
|
-
if (!name)
|
|
1016
|
-
return undefined;
|
|
1017
|
-
const literal = literalValue(property.initializer);
|
|
1018
|
-
if (literal !== undefined) {
|
|
1019
|
-
value[name] = literal;
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
const bound = valueExpr(property.initializer, setters, locals);
|
|
1023
|
-
if (bound?.expr.kind === "lit") {
|
|
1024
|
-
value[name] = bound.expr.value;
|
|
1025
|
-
bound.reads.forEach((read) => reads.add(read));
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
value[name] = firstValue(fields[name] ?? { kind: "tokens", count: 1 });
|
|
1029
|
-
}
|
|
1030
|
-
return { expr: { kind: "lit", value }, reads: [...reads] };
|
|
1031
|
-
}
|
|
1032
|
-
function taggedFieldsForObject(expression, domain) {
|
|
1033
|
-
const tagProperty = expression.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property.name) === domain.tag);
|
|
1034
|
-
const tag = tagProperty ? literalValue(tagProperty.initializer) : undefined;
|
|
1035
|
-
const variant = typeof tag === "string" ? domain.variants[tag] : undefined;
|
|
1036
|
-
return variant?.kind === "record" ? variant.fields : {};
|
|
1037
|
-
}
|
|
1038
|
-
function valueExpr(expression, setters, locals) {
|
|
1039
|
-
const value = literalValue(expression);
|
|
1040
|
-
if (value !== undefined)
|
|
1041
|
-
return { expr: { kind: "lit", value }, reads: [] };
|
|
1042
|
-
if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
|
|
1043
|
-
return modeledReadExpr(expression, setters, locals);
|
|
1044
|
-
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
|
|
1045
|
-
const parsed = booleanExpr(expression.operand, setters, locals);
|
|
1046
|
-
return parsed ? { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads } : undefined;
|
|
1047
|
-
}
|
|
1048
|
-
if (ts.isParenthesizedExpression(expression))
|
|
1049
|
-
return valueExpr(expression.expression, setters, locals);
|
|
1050
|
-
if (ts.isBinaryExpression(expression) &&
|
|
1051
|
-
(expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
|
|
1052
|
-
return booleanExpr(expression, setters, locals);
|
|
1053
|
-
}
|
|
1054
|
-
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1055
|
-
return nullishOptionalReadExpr(expression, setters, locals);
|
|
1056
|
-
}
|
|
1057
|
-
if (ts.isConditionalExpression(expression)) {
|
|
1058
|
-
const condition = booleanExpr(expression.condition, setters, locals);
|
|
1059
|
-
const whenTrue = valueExpr(expression.whenTrue, setters, locals);
|
|
1060
|
-
const whenFalse = valueExpr(expression.whenFalse, setters, locals);
|
|
1061
|
-
if (!condition || !whenTrue || !whenFalse)
|
|
1062
|
-
return undefined;
|
|
1063
|
-
return {
|
|
1064
|
-
expr: { kind: "cond", args: [condition.expr, whenTrue.expr, whenFalse.expr] },
|
|
1065
|
-
reads: [...new Set([...condition.reads, ...whenTrue.reads, ...whenFalse.reads])]
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
if (ts.isObjectLiteralExpression(expression))
|
|
1069
|
-
return objectSpreadUpdateExpr(expression, setters, locals);
|
|
1070
|
-
return undefined;
|
|
1071
|
-
}
|
|
1072
|
-
function objectSpreadUpdateExpr(expression, setters, locals) {
|
|
1073
|
-
if (expression.properties.length < 2)
|
|
1074
|
-
return undefined;
|
|
1075
|
-
const [spread, ...properties] = expression.properties;
|
|
1076
|
-
if (!ts.isSpreadAssignment(spread))
|
|
1077
|
-
return undefined;
|
|
1078
|
-
let current = valueExpr(spread.expression, setters, locals);
|
|
1079
|
-
if (!current)
|
|
1080
|
-
return undefined;
|
|
1081
|
-
const reads = new Set(current.reads);
|
|
1082
|
-
for (const property of properties) {
|
|
1083
|
-
if (!ts.isPropertyAssignment(property))
|
|
1084
|
-
return undefined;
|
|
1085
|
-
const name = propertyName(property.name);
|
|
1086
|
-
if (!name)
|
|
1087
|
-
return undefined;
|
|
1088
|
-
const value = valueExpr(property.initializer, setters, locals);
|
|
1089
|
-
if (!value)
|
|
1090
|
-
return undefined;
|
|
1091
|
-
value.reads.forEach((read) => reads.add(read));
|
|
1092
|
-
current = {
|
|
1093
|
-
expr: { kind: "updateField", target: current.expr, path: [name], value: value.expr },
|
|
1094
|
-
reads: [...reads]
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
return current;
|
|
1098
|
-
}
|
|
1099
|
-
function nullishOptionalReadExpr(expression, setters, locals = new Map()) {
|
|
1100
|
-
const fallback = literalValue(expression.right);
|
|
1101
|
-
if (fallback === undefined)
|
|
1102
|
-
return undefined;
|
|
1103
|
-
const read = optionalReadPath(expression.left);
|
|
1104
|
-
if (!read?.optional || read.path.length === 0)
|
|
1105
|
-
return undefined;
|
|
1106
|
-
const local = locals.get(read.base);
|
|
1107
|
-
const varId = local?.expr.kind === "read" ? local.expr.var : stateVarForName(read.base, setters);
|
|
1108
|
-
if (!varId)
|
|
1109
|
-
return undefined;
|
|
1110
|
-
const basePath = local?.expr.kind === "read" ? local.expr.path ?? [] : [];
|
|
1111
|
-
return {
|
|
1112
|
-
expr: {
|
|
1113
|
-
kind: "cond",
|
|
1114
|
-
args: [
|
|
1115
|
-
{ kind: "eq", args: [{ kind: "read", var: varId, ...(basePath.length > 0 ? { path: basePath } : {}) }, { kind: "lit", value: null }] },
|
|
1116
|
-
{ kind: "lit", value: fallback },
|
|
1117
|
-
{ kind: "read", var: varId, path: [...basePath, ...read.path] }
|
|
1118
|
-
]
|
|
1119
|
-
},
|
|
1120
|
-
reads: [varId]
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
function optionalReadPath(expression) {
|
|
1124
|
-
if (ts.isIdentifier(expression))
|
|
1125
|
-
return { base: expression.text, path: [], optional: false };
|
|
1126
|
-
if (isPropertyAccessLike(expression)) {
|
|
1127
|
-
const base = optionalReadPath(expression.expression);
|
|
1128
|
-
if (!base)
|
|
1129
|
-
return undefined;
|
|
1130
|
-
return {
|
|
1131
|
-
base: base.base,
|
|
1132
|
-
path: [...base.path, expression.name.text],
|
|
1133
|
-
optional: base.optional || Boolean(expression.questionDotToken)
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
return undefined;
|
|
1137
|
-
}
|
|
1138
|
-
function propertyName(name) {
|
|
1139
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
1140
|
-
return name.text;
|
|
1141
|
-
return undefined;
|
|
1142
|
-
}
|
|
1143
|
-
function booleanExpr(expression, setters, locals) {
|
|
1144
|
-
if (expression.kind === ts.SyntaxKind.TrueKeyword)
|
|
1145
|
-
return { expr: { kind: "lit", value: true }, reads: [] };
|
|
1146
|
-
if (expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
1147
|
-
return { expr: { kind: "lit", value: false }, reads: [] };
|
|
1148
|
-
if (ts.isIdentifier(expression))
|
|
1149
|
-
return valueExpr(expression, setters, locals);
|
|
1150
|
-
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
|
|
1151
|
-
const parsed = booleanExpr(expression.operand, setters, locals);
|
|
1152
|
-
return parsed ? { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads } : undefined;
|
|
1153
|
-
}
|
|
1154
|
-
if (ts.isParenthesizedExpression(expression))
|
|
1155
|
-
return booleanExpr(expression.expression, setters, locals);
|
|
1156
|
-
if (ts.isBinaryExpression(expression)) {
|
|
1157
|
-
if (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1158
|
-
const left = booleanExpr(expression.left, setters, locals);
|
|
1159
|
-
const right = booleanExpr(expression.right, setters, locals);
|
|
1160
|
-
if (!left || !right)
|
|
1161
|
-
return undefined;
|
|
1162
|
-
return {
|
|
1163
|
-
expr: { kind: expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ? "and" : "or", args: [left.expr, right.expr] },
|
|
1164
|
-
reads: [...new Set([...left.reads, ...right.reads])]
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
if (expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
1168
|
-
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
|
|
1169
|
-
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
1170
|
-
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
1171
|
-
const left = valueExpr(expression.left, setters, locals);
|
|
1172
|
-
const right = valueExpr(expression.right, setters, locals);
|
|
1173
|
-
if (!left || !right)
|
|
1174
|
-
return undefined;
|
|
1175
|
-
return {
|
|
1176
|
-
expr: {
|
|
1177
|
-
kind: expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken || expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken ? "neq" : "eq",
|
|
1178
|
-
args: [left.expr, right.expr]
|
|
1179
|
-
},
|
|
1180
|
-
reads: [...new Set([...left.reads, ...right.reads])]
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return undefined;
|
|
1185
|
-
}
|
|
1186
|
-
function modeledReadExpr(expression, setters, locals) {
|
|
1187
|
-
const path = propertyAccessPath(expression);
|
|
1188
|
-
if (!path || path.length === 0)
|
|
1189
|
-
return undefined;
|
|
1190
|
-
const [base, ...segments] = path;
|
|
1191
|
-
const local = locals.get(base);
|
|
1192
|
-
if (local) {
|
|
1193
|
-
if (segments.length === 0)
|
|
1194
|
-
return local;
|
|
1195
|
-
if (local.expr.kind !== "read")
|
|
1196
|
-
return undefined;
|
|
1197
|
-
return {
|
|
1198
|
-
expr: { kind: "read", var: local.expr.var, path: [...(local.expr.path ?? []), ...segments] },
|
|
1199
|
-
reads: local.reads
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
const setter = setterForName(base, setters);
|
|
1203
|
-
const stateVar = setter?.varId;
|
|
1204
|
-
if (!stateVar)
|
|
1205
|
-
return undefined;
|
|
1206
|
-
if (setter.domain.kind === "tagged" && segments.length > 0 && segments[0] !== setter.domain.tag) {
|
|
1207
|
-
return { expr: { kind: "lit", value: firstValue(taggedPathDomain(setter.domain, segments) ?? { kind: "tokens", count: 1 }) }, reads: [] };
|
|
1208
|
-
}
|
|
1209
|
-
return {
|
|
1210
|
-
expr: { kind: "read", var: stateVar, ...(segments.length > 0 ? { path: segments } : {}) },
|
|
1211
|
-
reads: [stateVar]
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
function readBinding(varId) {
|
|
1215
|
-
return { expr: { kind: "read", var: varId }, reads: [varId] };
|
|
1216
|
-
}
|
|
1217
|
-
function conditionalTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator) {
|
|
1218
|
-
const body = handler.body;
|
|
1219
|
-
if (!ts.isBlock(body) || body.statements.length !== 1 || !ts.isIfStatement(body.statements[0]))
|
|
1220
|
-
return undefined;
|
|
1221
|
-
const statement = body.statements[0];
|
|
1222
|
-
const condition = parseGuardExpression(statement.expression, setters);
|
|
1223
|
-
if (!condition)
|
|
1224
|
-
return undefined;
|
|
1225
|
-
const thenEffect = singleSetterEffect(statement.thenStatement, setters) ?? identityEffect();
|
|
1226
|
-
const elseEffect = statement.elseStatement ? singleSetterEffect(statement.elseStatement, setters) ?? identityEffect() : identityEffect();
|
|
1227
|
-
if (thenEffect.kind === "seq" && elseEffect.kind === "seq")
|
|
1228
|
-
return undefined;
|
|
1229
|
-
const writes = [...new Set([...effectWriteVars(thenEffect), ...effectWriteVars(elseEffect)])];
|
|
1230
|
-
const suffix = writes.map((id) => stateNameForVar(id, setters) ?? safeId(id)).join("_") || "if";
|
|
1231
|
-
return {
|
|
1232
|
-
id: `${component}.${attr}.${suffix}.if`,
|
|
1233
|
-
cls: "user",
|
|
1234
|
-
label: labelForEvent(attr, locator),
|
|
1235
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1236
|
-
guard: { kind: "lit", value: true },
|
|
1237
|
-
effect: { kind: "if", cond: condition.expr, then: thenEffect, else: elseEffect },
|
|
1238
|
-
reads: condition.reads,
|
|
1239
|
-
writes,
|
|
1240
|
-
confidence: "exact"
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
function singleSetterEffect(statement, setters) {
|
|
1244
|
-
if (ts.isBlock(statement) && statement.statements.length === 1)
|
|
1245
|
-
return setterAssignEffect(statement.statements[0], setters);
|
|
1246
|
-
return setterAssignEffect(statement, setters);
|
|
1247
|
-
}
|
|
1248
|
-
function identityEffect() {
|
|
1249
|
-
return { kind: "seq", effects: [] };
|
|
1250
|
-
}
|
|
1251
|
-
function effectWriteVars(effect) {
|
|
1252
|
-
if (effect.kind === "assign" || effect.kind === "havoc" || effect.kind === "choose")
|
|
1253
|
-
return [effect.var];
|
|
1254
|
-
if (effect.kind === "seq")
|
|
1255
|
-
return effect.effects.flatMap(effectWriteVars);
|
|
1256
|
-
if (effect.kind === "if")
|
|
1257
|
-
return [...effectWriteVars(effect.then), ...effectWriteVars(effect.else)];
|
|
1258
|
-
if (effect.kind === "enqueue" || effect.kind === "dequeue")
|
|
1259
|
-
return ["sys:pending"];
|
|
1260
|
-
if (effect.kind === "navigate")
|
|
1261
|
-
return ["sys:route", "sys:history"];
|
|
1262
|
-
return [...effect.ref.declaredWrites];
|
|
1263
|
-
}
|
|
1264
|
-
function stateNameForVar(varId, setters) {
|
|
1265
|
-
return [...setters.values()].find((setter) => setter.varId === varId)?.stateName;
|
|
1266
|
-
}
|
|
1267
|
-
function componentGuardLocalsFor(attribute, setters) {
|
|
1268
|
-
const body = enclosingFunctionBody(attribute);
|
|
1269
|
-
if (!body)
|
|
1270
|
-
return new Map();
|
|
1271
|
-
const locals = new Map();
|
|
1272
|
-
for (const statement of body.statements) {
|
|
1273
|
-
if (statement.pos > attribute.pos)
|
|
1274
|
-
break;
|
|
1275
|
-
if (ts.isReturnStatement(statement))
|
|
1276
|
-
break;
|
|
1277
|
-
bindConstStatement(statement, setters, locals, true);
|
|
1278
|
-
}
|
|
1279
|
-
return locals;
|
|
1280
|
-
}
|
|
1281
|
-
function componentScopeLocalsFor(attribute, setters, contextBindings) {
|
|
1282
|
-
const body = enclosingFunctionBody(attribute);
|
|
1283
|
-
if (!body)
|
|
1284
|
-
return new Map();
|
|
1285
|
-
const locals = new Map();
|
|
1286
|
-
for (const statement of body.statements) {
|
|
1287
|
-
if (statement.pos > attribute.pos)
|
|
1288
|
-
break;
|
|
1289
|
-
if (ts.isReturnStatement(statement))
|
|
1290
|
-
break;
|
|
1291
|
-
bindConstStatement(statement, setters, locals, true);
|
|
1292
|
-
for (const declaration of variableDeclarations(statement)) {
|
|
1293
|
-
bindContextHookObjectDeclaration(declaration, contextBindings, setters);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
return locals;
|
|
1297
|
-
}
|
|
1298
|
-
function variableDeclarations(node) {
|
|
1299
|
-
if (!ts.isVariableStatement(node))
|
|
1300
|
-
return [];
|
|
1301
|
-
return [...node.declarationList.declarations];
|
|
1302
|
-
}
|
|
1303
|
-
function enclosingFunctionBody(node) {
|
|
1304
|
-
let current = node;
|
|
1305
|
-
while (current) {
|
|
1306
|
-
if ((ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) && current.body && ts.isBlock(current.body)) {
|
|
1307
|
-
return current.body;
|
|
1308
|
-
}
|
|
1309
|
-
current = current.parent;
|
|
1310
|
-
}
|
|
1311
|
-
return undefined;
|
|
1312
|
-
}
|
|
1313
|
-
function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
|
|
1314
|
-
const body = handler.body;
|
|
1315
|
-
if (ts.isCallExpression(body))
|
|
1316
|
-
return { call: body, locals: new Map(initialLocals) };
|
|
1317
|
-
if (ts.isVoidExpression(body) && ts.isCallExpression(body.expression))
|
|
1318
|
-
return { call: body.expression, locals: new Map(initialLocals) };
|
|
1319
|
-
if (ts.isBlock(body)) {
|
|
1320
|
-
const locals = new Map(initialLocals);
|
|
1321
|
-
for (let index = 0; index < body.statements.length; index += 1) {
|
|
1322
|
-
const statement = body.statements[index];
|
|
1323
|
-
const isLast = index === body.statements.length - 1;
|
|
1324
|
-
if (isLast && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
|
|
1325
|
-
return { call: statement.expression, locals };
|
|
1326
|
-
if (isLast && ts.isExpressionStatement(statement) && ts.isVoidExpression(statement.expression) && ts.isCallExpression(statement.expression.expression))
|
|
1327
|
-
return { call: statement.expression.expression, locals };
|
|
1328
|
-
if (!bindConstStatement(statement, setters, locals))
|
|
1329
|
-
return undefined;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return undefined;
|
|
1333
|
-
}
|
|
1334
|
-
function pluginWriteTransition(source, fileName, node, attr, component, call, setters, locals, sourcePlugins, locator) {
|
|
1335
|
-
const callee = callName(call.expression);
|
|
1336
|
-
if (!callee)
|
|
1337
|
-
return undefined;
|
|
1338
|
-
const ctx = {
|
|
1339
|
-
read: (name, path) => {
|
|
1340
|
-
const local = locals.get(name);
|
|
1341
|
-
if (local?.expr.kind === "read") {
|
|
1342
|
-
return { kind: "read", var: local.expr.var, path: [...(local.expr.path ?? []), ...(path ?? [])] };
|
|
1343
|
-
}
|
|
1344
|
-
const varId = stateVarForName(name, setters) ?? name;
|
|
1345
|
-
return { kind: "read", var: varId, ...(path && path.length > 0 ? { path } : {}) };
|
|
1346
|
-
},
|
|
1347
|
-
locator
|
|
1348
|
-
};
|
|
1349
|
-
const callSite = {
|
|
1350
|
-
callee,
|
|
1351
|
-
arguments: call.arguments.map(callArgumentValue),
|
|
1352
|
-
source: { file: fileName, ...lineAndColumn(source, call) }
|
|
1353
|
-
};
|
|
1354
|
-
for (const plugin of sourcePlugins) {
|
|
1355
|
-
const summary = plugin.summarizeWrite?.(callSite, ctx);
|
|
1356
|
-
if (!summary || summary === "unsupported")
|
|
1357
|
-
continue;
|
|
1358
|
-
const reads = [...effectReads(summary)].sort();
|
|
1359
|
-
const writes = [...effectWrites(summary)].sort();
|
|
1360
|
-
return {
|
|
1361
|
-
id: `${component}.${attr}.${safeId(plugin.id)}.${safeId(callee)}`,
|
|
1362
|
-
cls: "user",
|
|
1363
|
-
label: labelForEvent(attr, locator),
|
|
1364
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1365
|
-
guard: { kind: "lit", value: true },
|
|
1366
|
-
effect: summary,
|
|
1367
|
-
reads,
|
|
1368
|
-
writes,
|
|
1369
|
-
confidence: "exact"
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
return undefined;
|
|
1373
|
-
}
|
|
1374
|
-
function callArgumentValue(argument) {
|
|
1375
|
-
const literal = literalValue(argument);
|
|
1376
|
-
if (literal !== undefined)
|
|
1377
|
-
return literal;
|
|
1378
|
-
if (ts.isIdentifier(argument))
|
|
1379
|
-
return argument.text;
|
|
1380
|
-
if (ts.isObjectLiteralExpression(argument)) {
|
|
1381
|
-
const fields = {};
|
|
1382
|
-
for (const property of argument.properties) {
|
|
1383
|
-
if (!ts.isPropertyAssignment(property))
|
|
1384
|
-
return argument.getText();
|
|
1385
|
-
const name = propertyName(property.name);
|
|
1386
|
-
if (!name)
|
|
1387
|
-
return argument.getText();
|
|
1388
|
-
fields[name] = callArgumentValue(property.initializer);
|
|
1389
|
-
}
|
|
1390
|
-
return fields;
|
|
1391
|
-
}
|
|
1392
|
-
return argument.getText();
|
|
1393
|
-
}
|
|
1394
|
-
function swrMutateTransition(source, fileName, node, attr, component, call, locator) {
|
|
1395
|
-
if (!ts.isIdentifier(call.expression) || call.expression.text !== "mutate")
|
|
1396
|
-
return undefined;
|
|
1397
|
-
return {
|
|
1398
|
-
id: `${component}.${attr}.mutate`,
|
|
1399
|
-
cls: "user",
|
|
1400
|
-
label: labelForEvent(attr, locator),
|
|
1401
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1402
|
-
guard: { kind: "lit", value: true },
|
|
1403
|
-
effect: { kind: "seq", effects: [] },
|
|
1404
|
-
reads: [],
|
|
1405
|
-
writes: [],
|
|
1406
|
-
confidence: "exact"
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
function noopCallTransition(source, fileName, node, attr, component, call, locator) {
|
|
1410
|
-
const name = callName(call.expression) ?? call.expression.getText(source);
|
|
1411
|
-
if (!isKnownPureUiCall(name))
|
|
1412
|
-
return undefined;
|
|
1413
|
-
return {
|
|
1414
|
-
id: `${component}.${attr}.${safeId(name)}.noop`,
|
|
1415
|
-
cls: "user",
|
|
1416
|
-
label: labelForEvent(attr, locator),
|
|
1417
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1418
|
-
guard: { kind: "lit", value: true },
|
|
1419
|
-
effect: { kind: "seq", effects: [] },
|
|
1420
|
-
reads: [],
|
|
1421
|
-
writes: [],
|
|
1422
|
-
confidence: "exact"
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
function isKnownPureUiCall(name) {
|
|
1426
|
-
return name.endsWith(".click") || name === "confirm" || name === "navigator.clipboard.writeText" || name.endsWith(".writeText");
|
|
1427
|
-
}
|
|
1428
|
-
function bindConstStatement(statement, setters, locals, partialBoolean = false) {
|
|
1429
|
-
if (!ts.isVariableStatement(statement))
|
|
1430
|
-
return false;
|
|
1431
|
-
if ((ts.getCombinedNodeFlags(statement.declarationList) & ts.NodeFlags.Const) === 0)
|
|
1432
|
-
return false;
|
|
1433
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
1434
|
-
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
1435
|
-
return false;
|
|
1436
|
-
const setterAlias = ts.isIdentifier(declaration.initializer) ? setters.get(declaration.initializer.text) ?? locals.get(declaration.initializer.text)?.setter : undefined;
|
|
1437
|
-
const binding = setterAlias ? { expr: { kind: "lit", value: null }, reads: [], setter: setterAlias } : valueExpr(declaration.initializer, setters, locals) ??
|
|
1438
|
-
(partialBoolean ? parseConjunctiveGuardExpression(declaration.initializer, setters, locals) : booleanExpr(declaration.initializer, setters, locals));
|
|
1439
|
-
if (!binding)
|
|
1440
|
-
return false;
|
|
1441
|
-
locals.set(declaration.name.text, binding);
|
|
1442
|
-
}
|
|
1443
|
-
return true;
|
|
1444
|
-
}
|
|
1445
|
-
function inlinedHelperCall(call, handlers, setters) {
|
|
1446
|
-
if (!ts.isIdentifier(call.expression) || call.arguments.length !== 0)
|
|
1447
|
-
return undefined;
|
|
1448
|
-
const helper = handlers.get(call.expression.text);
|
|
1449
|
-
return helper ? callSummaryFromHandler(helper, setters) : undefined;
|
|
1450
|
-
}
|
|
1451
|
-
function navigationTransition(source, fileName, node, attr, component, call, locator, routerPlugin, routePatterns = []) {
|
|
1452
|
-
const navigation = navigationCall(call, routerPlugin, routePatterns);
|
|
1453
|
-
if (!navigation)
|
|
1454
|
-
return undefined;
|
|
1455
|
-
const routeId = navigation.to ? safeId(navigation.to) : "back";
|
|
1456
|
-
return {
|
|
1457
|
-
id: `${component}.${attr}.navigate.${routeId}`,
|
|
1458
|
-
cls: "nav",
|
|
1459
|
-
label: {
|
|
1460
|
-
kind: "navigate",
|
|
1461
|
-
mode: navigation.mode === "replace" ? "push" : navigation.mode,
|
|
1462
|
-
...(navigation.to ? { to: navigation.to } : {})
|
|
1463
|
-
},
|
|
1464
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1465
|
-
guard: { kind: "lit", value: true },
|
|
1466
|
-
effect: {
|
|
1467
|
-
kind: "navigate",
|
|
1468
|
-
mode: navigation.mode,
|
|
1469
|
-
...(navigation.to ? { to: { kind: "lit", value: navigation.to } } : {})
|
|
1470
|
-
},
|
|
1471
|
-
reads: navigation.mode === "push" || navigation.mode === "back" ? ["sys:route", "sys:history"] : ["sys:history"],
|
|
1472
|
-
writes: ["sys:route", "sys:history"],
|
|
1473
|
-
confidence: "exact"
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
function navigationCall(call, routerPlugin, routePatterns = []) {
|
|
1477
|
-
const name = callName(call.expression);
|
|
1478
|
-
if (!name)
|
|
1479
|
-
return undefined;
|
|
1480
|
-
const pluginNavigation = routerPlugin?.navigationCall(name, call.arguments.map(callArgumentValue));
|
|
1481
|
-
if (pluginNavigation && pluginNavigation !== "unsupported")
|
|
1482
|
-
return pluginNavigation;
|
|
1483
|
-
if (name === "navigate" && call.arguments.length === 1) {
|
|
1484
|
-
const to = routeTargetValue(call.arguments[0], routePatterns);
|
|
1485
|
-
return typeof to === "string" ? { mode: "push", to } : undefined;
|
|
1486
|
-
}
|
|
1487
|
-
if ((name.endsWith(".push") || name.endsWith(".replace")) && call.arguments.length === 1) {
|
|
1488
|
-
const to = routeTargetValue(call.arguments[0], routePatterns);
|
|
1489
|
-
if (typeof to !== "string")
|
|
1490
|
-
return undefined;
|
|
1491
|
-
return { mode: name.endsWith(".replace") ? "replace" : "push", to };
|
|
1492
|
-
}
|
|
1493
|
-
if (name.endsWith(".back") && call.arguments.length === 0) {
|
|
1494
|
-
return { mode: "back" };
|
|
1495
|
-
}
|
|
1496
|
-
return undefined;
|
|
1497
|
-
}
|
|
1498
|
-
function linkNavigationTransition(source, fileName, node, component, routePatterns) {
|
|
1499
|
-
if ((!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) || node.tagName.getText(source) !== "Link")
|
|
1500
|
-
return undefined;
|
|
1501
|
-
const toAttr = node.attributes.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === "to");
|
|
1502
|
-
if (!toAttr)
|
|
1503
|
-
return undefined;
|
|
1504
|
-
const to = jsxRouteTarget(toAttr, routePatterns);
|
|
1505
|
-
if (!to)
|
|
1506
|
-
return undefined;
|
|
1507
|
-
return {
|
|
1508
|
-
id: `${component}.Link.navigate.${safeId(to)}`,
|
|
1509
|
-
cls: "nav",
|
|
1510
|
-
label: { kind: "navigate", mode: "push", to },
|
|
1511
|
-
source: [{ file: fileName, ...lineAndColumn(source, toAttr) }],
|
|
1512
|
-
guard: { kind: "lit", value: true },
|
|
1513
|
-
effect: { kind: "navigate", mode: "push", to: { kind: "lit", value: to } },
|
|
1514
|
-
reads: ["sys:route", "sys:history"],
|
|
1515
|
-
writes: ["sys:route", "sys:history"],
|
|
1516
|
-
confidence: "exact"
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
function jsxRouteTarget(attribute, routePatterns) {
|
|
1520
|
-
if (!attribute.initializer)
|
|
1521
|
-
return undefined;
|
|
1522
|
-
if (ts.isStringLiteral(attribute.initializer))
|
|
1523
|
-
return normalizeRouteTarget(attribute.initializer.text, routePatterns);
|
|
1524
|
-
if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression)
|
|
1525
|
-
return undefined;
|
|
1526
|
-
return routeTargetValue(attribute.initializer.expression, routePatterns);
|
|
1527
|
-
}
|
|
1528
|
-
function routeTargetValue(expression, routePatterns) {
|
|
1529
|
-
if (!expression)
|
|
1530
|
-
return undefined;
|
|
1531
|
-
const literal = literalValue(expression);
|
|
1532
|
-
if (typeof literal === "string")
|
|
1533
|
-
return normalizeRouteTarget(literal, routePatterns);
|
|
1534
|
-
if (ts.isNoSubstitutionTemplateLiteral(expression))
|
|
1535
|
-
return normalizeRouteTarget(expression.text, routePatterns);
|
|
1536
|
-
if (ts.isTemplateExpression(expression)) {
|
|
1537
|
-
const pattern = templateRoutePattern(expression);
|
|
1538
|
-
return pattern ? normalizeRouteTarget(pattern, routePatterns) : undefined;
|
|
1539
|
-
}
|
|
1540
|
-
return undefined;
|
|
1541
|
-
}
|
|
1542
|
-
function templateRoutePattern(expression) {
|
|
1543
|
-
let value = expression.head.text;
|
|
1544
|
-
for (const span of expression.templateSpans)
|
|
1545
|
-
value += ":param" + span.literal.text;
|
|
1546
|
-
return value;
|
|
1547
|
-
}
|
|
1548
|
-
function normalizeRouteTarget(target, routePatterns) {
|
|
1549
|
-
const slash = target.startsWith("/") ? target : `/${target}`;
|
|
1550
|
-
const matched = routePatterns.find((pattern) => routePatternMatches(pattern, slash));
|
|
1551
|
-
return matched ?? slash.replace(/\/:param(?=\/|$)/g, "/:id");
|
|
1552
|
-
}
|
|
1553
|
-
function routePatternMatches(pattern, target) {
|
|
1554
|
-
const left = pattern.replace(/^\/+/, "").split("/");
|
|
1555
|
-
const right = target.replace(/^\/+/, "").split("/");
|
|
1556
|
-
if (left.length !== right.length)
|
|
1557
|
-
return false;
|
|
1558
|
-
return left.every((part, index) => part.startsWith(":") || part === "*" || part === right[index] || right[index] === ":param");
|
|
1559
|
-
}
|
|
1560
|
-
function escapedSetters(call, setters, locals = new Map()) {
|
|
1561
|
-
return call.arguments
|
|
1562
|
-
.filter(ts.isIdentifier)
|
|
1563
|
-
.map((arg) => setters.get(arg.text) ?? locals.get(arg.text)?.setter)
|
|
1564
|
-
.filter((setter) => Boolean(setter));
|
|
1565
|
-
}
|
|
1566
|
-
function escapedSetterTransitions(source, fileName, node, attr, component, setters, locator) {
|
|
1567
|
-
return setters.map((setter) => ({
|
|
1568
|
-
id: `${component}.${attr}.${setter.stateName}.escaped`,
|
|
1569
|
-
cls: "user",
|
|
1570
|
-
label: labelForEvent(attr, locator),
|
|
1571
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1572
|
-
guard: { kind: "lit", value: true },
|
|
1573
|
-
effect: { kind: "havoc", var: setter.varId },
|
|
1574
|
-
reads: [],
|
|
1575
|
-
writes: [setter.varId],
|
|
1576
|
-
confidence: "over-approx"
|
|
1577
|
-
}));
|
|
1578
|
-
}
|
|
1579
|
-
function inputTransitions(source, fileName, node, attr, component, setter, locator) {
|
|
1580
|
-
const literalValues = literalInputValues(node);
|
|
1581
|
-
const finite = literalValues
|
|
1582
|
-
? finiteInputValues(setter.domain).filter(({ valueClass }) => literalValues.has(valueClass))
|
|
1583
|
-
: finiteInputValues(setter.domain);
|
|
1584
|
-
if (finite.length > 0) {
|
|
1585
|
-
return finite.map(({ value, valueClass }) => ({
|
|
1586
|
-
id: `${component}.${attr}.${setter.stateName}.${safeId(valueClass)}`,
|
|
1587
|
-
cls: "user",
|
|
1588
|
-
label: { kind: "input", valueClass, ...(locator ? { locator } : {}) },
|
|
1589
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1590
|
-
guard: { kind: "lit", value: true },
|
|
1591
|
-
effect: { kind: "assign", var: setter.varId, expr: { kind: "lit", value } },
|
|
1592
|
-
reads: [],
|
|
1593
|
-
writes: [setter.varId],
|
|
1594
|
-
confidence: "exact"
|
|
1595
|
-
}));
|
|
1596
|
-
}
|
|
1597
|
-
return [{
|
|
1598
|
-
id: `${component}.${attr}.${setter.stateName}`,
|
|
1599
|
-
cls: "user",
|
|
1600
|
-
label: { kind: "input", valueClass: valueClassForDomain(setter.domain), ...(locator ? { locator } : {}) },
|
|
1601
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
1602
|
-
guard: { kind: "lit", value: true },
|
|
1603
|
-
effect: { kind: "havoc", var: setter.varId },
|
|
1604
|
-
reads: [],
|
|
1605
|
-
writes: [setter.varId],
|
|
1606
|
-
confidence: "over-approx"
|
|
1607
|
-
}];
|
|
1608
|
-
}
|
|
1609
|
-
function literalInputValues(attribute) {
|
|
1610
|
-
return selectOptionValues(attribute) ?? radioInputValue(attribute);
|
|
1611
|
-
}
|
|
1612
|
-
function selectOptionValues(attribute) {
|
|
1613
|
-
const attrs = attribute.parent;
|
|
1614
|
-
if (!ts.isJsxAttributes(attrs))
|
|
1615
|
-
return undefined;
|
|
1616
|
-
const opening = attrs.parent;
|
|
1617
|
-
if (!ts.isJsxOpeningElement(opening) || opening.tagName.getText() !== "select" || !ts.isJsxElement(opening.parent))
|
|
1618
|
-
return undefined;
|
|
1619
|
-
const values = opening.parent.children
|
|
1620
|
-
.filter(ts.isJsxElement)
|
|
1621
|
-
.filter((child) => child.openingElement.tagName.getText() === "option")
|
|
1622
|
-
.map((child) => optionValue(child))
|
|
1623
|
-
.filter((value) => Boolean(value));
|
|
1624
|
-
return values.length > 0 ? new Set(values) : undefined;
|
|
1625
|
-
}
|
|
1626
|
-
function optionValue(option) {
|
|
1627
|
-
const value = stringAttribute(option.openingElement.attributes, "value");
|
|
1628
|
-
if (value)
|
|
1629
|
-
return value;
|
|
1630
|
-
return simpleElementText(option.openingElement);
|
|
1631
|
-
}
|
|
1632
|
-
function radioInputValue(attribute) {
|
|
1633
|
-
const attrs = attribute.parent;
|
|
1634
|
-
if (!ts.isJsxAttributes(attrs))
|
|
1635
|
-
return undefined;
|
|
1636
|
-
const opening = attrs.parent;
|
|
1637
|
-
if (!ts.isJsxOpeningElement(opening) && !ts.isJsxSelfClosingElement(opening))
|
|
1638
|
-
return undefined;
|
|
1639
|
-
if (opening.tagName.getText() !== "input" || stringAttribute(attrs, "type") !== "radio")
|
|
1640
|
-
return undefined;
|
|
1641
|
-
const value = stringAttribute(attrs, "value");
|
|
1642
|
-
return value ? new Set([value]) : undefined;
|
|
1643
|
-
}
|
|
1644
|
-
function finiteInputValues(domain) {
|
|
1645
|
-
if (domain.kind === "enum")
|
|
1646
|
-
return domain.values.map((value) => ({ value, valueClass: value }));
|
|
1647
|
-
if (domain.kind === "boundedInt") {
|
|
1648
|
-
return Array.from({ length: domain.max - domain.min + 1 }, (_, index) => {
|
|
1649
|
-
const value = domain.min + index;
|
|
1650
|
-
return { value, valueClass: String(value) };
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
if (domain.kind === "bool") {
|
|
1654
|
-
return [
|
|
1655
|
-
{ value: false, valueClass: "false" },
|
|
1656
|
-
{ value: true, valueClass: "true" }
|
|
1657
|
-
];
|
|
1658
|
-
}
|
|
1659
|
-
return [];
|
|
1660
|
-
}
|
|
1661
|
-
function handlerExpression(expression, handlers) {
|
|
1662
|
-
if (!expression)
|
|
1663
|
-
return undefined;
|
|
1664
|
-
if (isExtractableHandler(expression))
|
|
1665
|
-
return expression;
|
|
1666
|
-
if (ts.isIdentifier(expression))
|
|
1667
|
-
return handlers.get(expression.text);
|
|
1668
|
-
return undefined;
|
|
1669
|
-
}
|
|
1670
|
-
function componentDeclarations(source) {
|
|
1671
|
-
const components = new Map();
|
|
1672
|
-
const visit = (node) => {
|
|
1673
|
-
if (ts.isFunctionDeclaration(node) && node.name && startsUppercase(node.name.text)) {
|
|
1674
|
-
components.set(node.name.text, node);
|
|
1675
|
-
}
|
|
1676
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && startsUppercase(node.name.text) && node.initializer && isExtractableHandler(node.initializer)) {
|
|
1677
|
-
components.set(node.name.text, node.initializer);
|
|
1678
|
-
}
|
|
1679
|
-
ts.forEachChild(node, visit);
|
|
1680
|
-
};
|
|
1681
|
-
visit(source);
|
|
1682
|
-
return components;
|
|
1683
|
-
}
|
|
1684
|
-
function customHookDeclarations(source) {
|
|
1685
|
-
const hooks = new Map();
|
|
1686
|
-
const visit = (node) => {
|
|
1687
|
-
const name = customHookDeclarationName(node);
|
|
1688
|
-
if (name) {
|
|
1689
|
-
if (ts.isFunctionDeclaration(node))
|
|
1690
|
-
hooks.set(name, node);
|
|
1691
|
-
else if (ts.isVariableDeclaration(node) && node.initializer && isExtractableHandler(node.initializer))
|
|
1692
|
-
hooks.set(name, node.initializer);
|
|
1693
|
-
}
|
|
1694
|
-
ts.forEachChild(node, visit);
|
|
1695
|
-
};
|
|
1696
|
-
visit(source);
|
|
1697
|
-
return hooks;
|
|
1698
|
-
}
|
|
1699
|
-
function isCustomHookDeclaration(node) {
|
|
1700
|
-
return Boolean(customHookDeclarationName(node));
|
|
1701
|
-
}
|
|
1702
|
-
function inlineCustomHookState(source, fileName, node, customHooks, vars, setters, component, route) {
|
|
1703
|
-
if (!ts.isArrayBindingPattern(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression))
|
|
1704
|
-
return false;
|
|
1705
|
-
const hook = customHooks.get(node.initializer.expression.text);
|
|
1706
|
-
if (!hook)
|
|
1707
|
-
return false;
|
|
1708
|
-
const stateName = node.name.elements[0];
|
|
1709
|
-
const setterName = node.name.elements[1];
|
|
1710
|
-
if (!ts.isBindingElement(stateName) || !ts.isIdentifier(stateName.name) || !ts.isBindingElement(setterName) || !ts.isIdentifier(setterName.name))
|
|
1711
|
-
return false;
|
|
1712
|
-
const summary = hookStateReturn(hook);
|
|
1713
|
-
if (!summary)
|
|
1714
|
-
return false;
|
|
1715
|
-
const varId = `local:${component}.${stateName.name.text}`;
|
|
1716
|
-
const decl = {
|
|
1717
|
-
id: varId,
|
|
1718
|
-
domain: summary.domain,
|
|
1719
|
-
origin: { file: fileName, ...lineAndColumn(source, node) },
|
|
1720
|
-
scope: { kind: "route-local", route },
|
|
1721
|
-
initial: summary.initial
|
|
1722
|
-
};
|
|
1723
|
-
vars.push(decl);
|
|
1724
|
-
setters.set(setterName.name.text, { varId, component, stateName: stateName.name.text, domain: summary.domain });
|
|
1725
|
-
return true;
|
|
1726
|
-
}
|
|
1727
|
-
function hookStateReturn(hook) {
|
|
1728
|
-
const body = hookBody(hook);
|
|
1729
|
-
if (!body)
|
|
1730
|
-
return undefined;
|
|
1731
|
-
let stateName;
|
|
1732
|
-
let setterName;
|
|
1733
|
-
let stateCall;
|
|
1734
|
-
for (const statement of body.statements) {
|
|
1735
|
-
if (!ts.isVariableStatement(statement))
|
|
1736
|
-
continue;
|
|
1737
|
-
for (const decl of statement.declarationList.declarations) {
|
|
1738
|
-
if (!ts.isArrayBindingPattern(decl.name) || !decl.initializer || !isUseStateCall(decl.initializer))
|
|
1739
|
-
continue;
|
|
1740
|
-
const state = decl.name.elements[0];
|
|
1741
|
-
const setter = decl.name.elements[1];
|
|
1742
|
-
if (!ts.isBindingElement(state) || !ts.isIdentifier(state.name) || !ts.isBindingElement(setter) || !ts.isIdentifier(setter.name))
|
|
1743
|
-
return undefined;
|
|
1744
|
-
if (stateCall)
|
|
1745
|
-
return undefined;
|
|
1746
|
-
stateName = state.name.text;
|
|
1747
|
-
setterName = setter.name.text;
|
|
1748
|
-
stateCall = decl.initializer;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
if (!stateName || !setterName || !stateCall)
|
|
1752
|
-
return undefined;
|
|
1753
|
-
const returned = body.statements.find(ts.isReturnStatement);
|
|
1754
|
-
if (!returned?.expression)
|
|
1755
|
-
return undefined;
|
|
1756
|
-
const elements = returnedArrayElements(returned.expression);
|
|
1757
|
-
if (!elements || elements.length < 2)
|
|
1758
|
-
return undefined;
|
|
1759
|
-
if (!ts.isIdentifier(elements[0]) || elements[0].text !== stateName || !ts.isIdentifier(elements[1]) || elements[1].text !== setterName)
|
|
1760
|
-
return undefined;
|
|
1761
|
-
const domain = inferUseStateDomain(stateCall);
|
|
1762
|
-
return { domain, initial: initialValueForUseState(stateCall, domain) };
|
|
1763
|
-
}
|
|
1764
|
-
function hookBody(hook) {
|
|
1765
|
-
if (ts.isFunctionDeclaration(hook))
|
|
1766
|
-
return hook.body;
|
|
1767
|
-
return ts.isBlock(hook.body) ? hook.body : undefined;
|
|
1768
|
-
}
|
|
1769
|
-
function returnedArrayElements(expression) {
|
|
1770
|
-
if (ts.isArrayLiteralExpression(expression))
|
|
1771
|
-
return expression.elements;
|
|
1772
|
-
if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
1773
|
-
return returnedArrayElements(expression.expression);
|
|
1774
|
-
return undefined;
|
|
1775
|
-
}
|
|
1776
|
-
function customHookDeclarationName(node) {
|
|
1777
|
-
if (ts.isFunctionDeclaration(node) && node.name && isCustomHookName(node.name.text))
|
|
1778
|
-
return node.name.text;
|
|
1779
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && isCustomHookName(node.name.text) && node.initializer && isExtractableHandler(node.initializer)) {
|
|
1780
|
-
return node.name.text;
|
|
1781
|
-
}
|
|
1782
|
-
return undefined;
|
|
1783
|
-
}
|
|
1784
|
-
function calledCustomHook(node, customHooks) {
|
|
1785
|
-
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression))
|
|
1786
|
-
return undefined;
|
|
1787
|
-
return customHooks.has(node.expression.text) ? node.expression.text : undefined;
|
|
1788
|
-
}
|
|
1789
|
-
function isCustomHookName(name) {
|
|
1790
|
-
return /^use[A-Z0-9]/.test(name) && name !== "useState" && name !== "useEffect" && name !== "useReducer" && name !== "useRef";
|
|
1791
|
-
}
|
|
1792
|
-
function detectStatefulListComponents(source, components) {
|
|
1793
|
-
const listComponents = new Set();
|
|
1794
|
-
const visit = (node) => {
|
|
1795
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map") {
|
|
1796
|
-
for (const rendered of jsxComponentTags(node)) {
|
|
1797
|
-
const component = components.get(rendered);
|
|
1798
|
-
if (component && componentHasUseState(component))
|
|
1799
|
-
listComponents.add(rendered);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
ts.forEachChild(node, visit);
|
|
1803
|
-
};
|
|
1804
|
-
visit(source);
|
|
1805
|
-
return listComponents;
|
|
1806
|
-
}
|
|
1807
|
-
function listRenderedHandlerInfo(attribute, vars, component) {
|
|
1808
|
-
let current = attribute;
|
|
1809
|
-
while (current.parent) {
|
|
1810
|
-
const parent = current.parent;
|
|
1811
|
-
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression) && parent.expression.name.text === "map") {
|
|
1812
|
-
const callback = parent.arguments[0];
|
|
1813
|
-
if (callback && current.pos >= callback.pos && current.end <= callback.end) {
|
|
1814
|
-
const receiver = parent.expression.expression;
|
|
1815
|
-
const itemName = mapItemName(callback);
|
|
1816
|
-
if (ts.isIdentifier(receiver) && itemName) {
|
|
1817
|
-
const info = stateVarInfoForName(receiver.text, vars, component);
|
|
1818
|
-
return info ? { ...info, itemName } : undefined;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
current = parent;
|
|
1823
|
-
}
|
|
1824
|
-
return undefined;
|
|
1825
|
-
}
|
|
1826
|
-
function mapItemName(callback) {
|
|
1827
|
-
if ((ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && callback.parameters.length > 0) {
|
|
1828
|
-
const name = callback.parameters[0]?.name;
|
|
1829
|
-
return name && ts.isIdentifier(name) ? name.text : undefined;
|
|
1830
|
-
}
|
|
1831
|
-
return undefined;
|
|
1832
|
-
}
|
|
1833
|
-
function stateVarInfoForName(name, vars, component) {
|
|
1834
|
-
const localId = `local:${component}.${name}`;
|
|
1835
|
-
const decl = vars.find((candidate) => candidate.id === localId);
|
|
1836
|
-
return decl ? { varId: decl.id, domain: decl.domain } : undefined;
|
|
1837
|
-
}
|
|
1838
|
-
function jsxComponentTags(node) {
|
|
1839
|
-
const tags = new Set();
|
|
1840
|
-
const visit = (candidate) => {
|
|
1841
|
-
const tag = jsxElementTag(candidate);
|
|
1842
|
-
if (tag && startsUppercase(tag))
|
|
1843
|
-
tags.add(tag);
|
|
1844
|
-
ts.forEachChild(candidate, visit);
|
|
1845
|
-
};
|
|
1846
|
-
visit(node);
|
|
1847
|
-
return [...tags].sort();
|
|
1848
|
-
}
|
|
1849
|
-
function jsxElementTag(node) {
|
|
1850
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1851
|
-
return ts.isIdentifier(node.tagName) ? node.tagName.text : undefined;
|
|
1852
|
-
}
|
|
1853
|
-
return undefined;
|
|
1854
|
-
}
|
|
1855
|
-
function componentHasUseState(component) {
|
|
1856
|
-
let found = false;
|
|
1857
|
-
const visit = (node) => {
|
|
1858
|
-
if (found)
|
|
1859
|
-
return;
|
|
1860
|
-
if (ts.isCallExpression(node) && isUseStateCall(node)) {
|
|
1861
|
-
found = true;
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
ts.forEachChild(node, visit);
|
|
1865
|
-
};
|
|
1866
|
-
visit(component);
|
|
1867
|
-
return found;
|
|
1868
|
-
}
|
|
1869
|
-
function componentPropTrigger(source, component, propName, setters, warnings) {
|
|
1870
|
-
const localHandlers = componentLocalHandlers(component);
|
|
1871
|
-
let trigger;
|
|
1872
|
-
const visit = (node) => {
|
|
1873
|
-
if (trigger)
|
|
1874
|
-
return;
|
|
1875
|
-
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isEventAttribute(node.name.text) && isIntrinsicJsxAttribute(node)) {
|
|
1876
|
-
const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
|
|
1877
|
-
const handler = handlerExpression(expression, localHandlers);
|
|
1878
|
-
if (expression && (expressionReferencesProp(expression, component, propName) || (handler && handlerCallsProp(handler, component, propName, localHandlers)))) {
|
|
1879
|
-
trigger = {
|
|
1880
|
-
attr: node.name.text,
|
|
1881
|
-
locator: locatorForEventAttribute(node),
|
|
1882
|
-
guard: disabledGuardFor(node, setters, warnings, source, componentName(component) ?? "Anonymous")
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
ts.forEachChild(node, visit);
|
|
1887
|
-
};
|
|
1888
|
-
visit(component);
|
|
1889
|
-
return trigger;
|
|
1890
|
-
}
|
|
1891
|
-
function transparentComponentPropTrigger(component, propName) {
|
|
1892
|
-
if (!isForwardablePropName(propName) || !componentSpreadsPropsToElement(component))
|
|
1893
|
-
return undefined;
|
|
1894
|
-
return { attr: propName };
|
|
1895
|
-
}
|
|
1896
|
-
function componentSpreadsPropsToElement(component) {
|
|
1897
|
-
let found = false;
|
|
1898
|
-
const visit = (node) => {
|
|
1899
|
-
if (found)
|
|
1900
|
-
return;
|
|
1901
|
-
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && node.attributes.properties.some(ts.isJsxSpreadAttribute)) {
|
|
1902
|
-
found = true;
|
|
1903
|
-
return;
|
|
1904
|
-
}
|
|
1905
|
-
ts.forEachChild(node, visit);
|
|
1906
|
-
};
|
|
1907
|
-
visit(component);
|
|
1908
|
-
return found;
|
|
1909
|
-
}
|
|
1910
|
-
function forwardsComponentProp(node, handlers, component) {
|
|
1911
|
-
if (!component || !node.initializer)
|
|
1912
|
-
return false;
|
|
1913
|
-
const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
|
|
1914
|
-
if (expression && expressionReferencesForwardableProp(expression, component))
|
|
1915
|
-
return true;
|
|
1916
|
-
const localHandlers = componentLocalHandlers(component);
|
|
1917
|
-
const handler = handlerExpression(expression, handlers) ?? handlerExpression(expression, localHandlers);
|
|
1918
|
-
return Boolean(handler && handlerCallsForwardableProp(handler, component, localHandlers));
|
|
1919
|
-
}
|
|
1920
|
-
function componentLocalHandlers(component) {
|
|
1921
|
-
const localHandlers = new Map();
|
|
1922
|
-
const visit = (node) => {
|
|
1923
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isExtractableHandler(node.initializer)) {
|
|
1924
|
-
localHandlers.set(node.name.text, node.initializer);
|
|
1925
|
-
}
|
|
1926
|
-
ts.forEachChild(node, visit);
|
|
1927
|
-
};
|
|
1928
|
-
visit(component);
|
|
1929
|
-
return localHandlers;
|
|
1930
|
-
}
|
|
1931
|
-
function handlerCallsProp(handler, component, propName, localHandlers, seen = new Set()) {
|
|
1932
|
-
if (seen.has(handler))
|
|
1933
|
-
return false;
|
|
1934
|
-
seen.add(handler);
|
|
1935
|
-
const aliases = componentPropAliases(component, propName);
|
|
1936
|
-
const propObjects = componentPropObjectNames(component);
|
|
1937
|
-
let found = false;
|
|
1938
|
-
const visit = (node) => {
|
|
1939
|
-
if (found)
|
|
1940
|
-
return;
|
|
1941
|
-
if (ts.isCallExpression(node)) {
|
|
1942
|
-
if (callInvokesProp(node.expression, propName, aliases, propObjects)) {
|
|
1943
|
-
found = true;
|
|
1944
|
-
return;
|
|
1945
|
-
}
|
|
1946
|
-
if (ts.isIdentifier(node.expression)) {
|
|
1947
|
-
const local = localHandlers.get(node.expression.text);
|
|
1948
|
-
if (local && handlerCallsProp(local, component, propName, localHandlers, seen)) {
|
|
1949
|
-
found = true;
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
ts.forEachChild(node, visit);
|
|
1955
|
-
};
|
|
1956
|
-
visit(handler.body);
|
|
1957
|
-
return found;
|
|
1958
|
-
}
|
|
1959
|
-
function handlerCallsForwardableProp(handler, component, localHandlers) {
|
|
1960
|
-
return forwardableComponentPropNames(component).some((propName) => handlerCallsProp(handler, component, propName, localHandlers));
|
|
1961
|
-
}
|
|
1962
|
-
function expressionReferencesForwardableProp(expression, component) {
|
|
1963
|
-
return forwardableComponentPropNames(component).some((propName) => expressionReferencesProp(expression, component, propName));
|
|
1964
|
-
}
|
|
1965
|
-
function expressionReferencesProp(expression, component, propName) {
|
|
1966
|
-
const aliases = componentPropAliases(component, propName);
|
|
1967
|
-
const propObjects = componentPropObjectNames(component);
|
|
1968
|
-
if (ts.isIdentifier(expression))
|
|
1969
|
-
return aliases.has(expression.text);
|
|
1970
|
-
if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== propName)
|
|
1971
|
-
return false;
|
|
1972
|
-
if (propObjects.size === 0)
|
|
1973
|
-
return true;
|
|
1974
|
-
return ts.isIdentifier(expression.expression) && propObjects.has(expression.expression.text);
|
|
1975
|
-
}
|
|
1976
|
-
function callInvokesProp(expression, propName, aliases, propObjects) {
|
|
1977
|
-
if (ts.isIdentifier(expression))
|
|
1978
|
-
return aliases.has(expression.text);
|
|
1979
|
-
if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== propName)
|
|
1980
|
-
return false;
|
|
1981
|
-
if (propObjects.size === 0)
|
|
1982
|
-
return true;
|
|
1983
|
-
return ts.isIdentifier(expression.expression) && propObjects.has(expression.expression.text);
|
|
1984
|
-
}
|
|
1985
|
-
function componentPropAliases(component, propName) {
|
|
1986
|
-
const aliases = new Set();
|
|
1987
|
-
const firstParam = component.parameters[0];
|
|
1988
|
-
if (!firstParam || !ts.isObjectBindingPattern(firstParam.name))
|
|
1989
|
-
return aliases;
|
|
1990
|
-
for (const element of firstParam.name.elements) {
|
|
1991
|
-
const name = element.name;
|
|
1992
|
-
if (!ts.isIdentifier(name))
|
|
1993
|
-
continue;
|
|
1994
|
-
const propertyName = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : name.text;
|
|
1995
|
-
if (propertyName === propName)
|
|
1996
|
-
aliases.add(name.text);
|
|
1997
|
-
}
|
|
1998
|
-
return aliases;
|
|
1999
|
-
}
|
|
2000
|
-
function forwardableComponentPropNames(component) {
|
|
2001
|
-
const names = new Set();
|
|
2002
|
-
const firstParam = component.parameters[0];
|
|
2003
|
-
if (!firstParam)
|
|
2004
|
-
return [];
|
|
2005
|
-
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
2006
|
-
for (const element of firstParam.name.elements) {
|
|
2007
|
-
const name = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : ts.isIdentifier(element.name) ? element.name.text : undefined;
|
|
2008
|
-
if (name && isForwardablePropName(name))
|
|
2009
|
-
names.add(name);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
if (ts.isIdentifier(firstParam.name)) {
|
|
2013
|
-
if (!component.body)
|
|
2014
|
-
return [...names].sort();
|
|
2015
|
-
const objectName = firstParam.name.text;
|
|
2016
|
-
const visit = (node) => {
|
|
2017
|
-
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === objectName && isForwardablePropName(node.name.text)) {
|
|
2018
|
-
names.add(node.name.text);
|
|
2019
|
-
}
|
|
2020
|
-
ts.forEachChild(node, visit);
|
|
2021
|
-
};
|
|
2022
|
-
visit(component.body);
|
|
2023
|
-
}
|
|
2024
|
-
return [...names].sort();
|
|
2025
|
-
}
|
|
2026
|
-
function componentPropObjectNames(component) {
|
|
2027
|
-
const firstParam = component.parameters[0];
|
|
2028
|
-
return new Set(firstParam && ts.isIdentifier(firstParam.name) ? [firstParam.name.text] : []);
|
|
2029
|
-
}
|
|
2030
|
-
function componentName(component) {
|
|
2031
|
-
if (ts.isFunctionDeclaration(component) && component.name)
|
|
2032
|
-
return component.name.text;
|
|
2033
|
-
return componentNameFor(component.parent);
|
|
2034
|
-
}
|
|
2035
|
-
function isForwardablePropName(name) {
|
|
2036
|
-
return /^on[A-Z]/.test(name);
|
|
2037
|
-
}
|
|
2038
|
-
function isIntrinsicJsxAttribute(attribute) {
|
|
2039
|
-
const attrs = attribute.parent;
|
|
2040
|
-
if (!ts.isJsxAttributes(attrs))
|
|
2041
|
-
return false;
|
|
2042
|
-
const parent = attrs.parent;
|
|
2043
|
-
if (!ts.isJsxOpeningElement(parent) && !ts.isJsxSelfClosingElement(parent))
|
|
2044
|
-
return false;
|
|
2045
|
-
const tag = parent.tagName;
|
|
2046
|
-
return ts.isIdentifier(tag) && !startsUppercase(tag.text);
|
|
2047
|
-
}
|
|
2048
|
-
function jsxTagName(attribute) {
|
|
2049
|
-
const attrs = attribute.parent;
|
|
2050
|
-
if (!ts.isJsxAttributes(attrs))
|
|
2051
|
-
return undefined;
|
|
2052
|
-
const parent = attrs.parent;
|
|
2053
|
-
if (!ts.isJsxOpeningElement(parent) && !ts.isJsxSelfClosingElement(parent))
|
|
2054
|
-
return undefined;
|
|
2055
|
-
return ts.isIdentifier(parent.tagName) ? parent.tagName.text : undefined;
|
|
2056
|
-
}
|
|
2057
|
-
function transitionsFromAsyncHandler(source, fileName, attr, expression, setters, component, effectApis, asyncOutcomes, locator, routePatterns, warnings) {
|
|
2058
|
-
if (!ts.isBlock(expression.body))
|
|
2059
|
-
return [];
|
|
2060
|
-
const statements = expression.body.statements;
|
|
2061
|
-
const tryStatement = statements.find(ts.isTryStatement);
|
|
2062
|
-
const awaitStatement = tryStatement
|
|
2063
|
-
? tryStatement.tryBlock.statements.find((statement) => expressionStatementAwait(statement, effectApis))
|
|
2064
|
-
: statements.find((statement) => expressionStatementAwait(statement, effectApis));
|
|
2065
|
-
if (!awaitStatement)
|
|
2066
|
-
return [];
|
|
2067
|
-
const awaited = awaitedCall(awaitStatement, effectApis);
|
|
2068
|
-
const op = awaited?.op;
|
|
2069
|
-
if (!op)
|
|
2070
|
-
return [];
|
|
2071
|
-
const opArgs = awaited ? effectCallArgs(awaited.call, setters, new Map()) : { args: {}, reads: [] };
|
|
2072
|
-
const preStatements = tryStatement ? statements.slice(0, statements.indexOf(tryStatement)) : statements.slice(0, statements.indexOf(awaitStatement));
|
|
2073
|
-
const preSummaries = summarizeAsyncSegment(preStatements, setters);
|
|
2074
|
-
const successStatements = tryStatement ? tryStatement.tryBlock.statements.slice(tryStatement.tryBlock.statements.indexOf(awaitStatement) + 1) : statements.slice(statements.indexOf(awaitStatement) + 1);
|
|
2075
|
-
if (!tryStatement) {
|
|
2076
|
-
const chained = transitionsFromSequentialAwait(source, fileName, attr, expression, awaitStatement, op, preSummaries, successStatements, setters, effectApis, component, locator, warnings);
|
|
2077
|
-
if (chained.length > 0)
|
|
2078
|
-
return chained;
|
|
2079
|
-
}
|
|
2080
|
-
if (containsAwaitedEffect(successStatements, effectApis) || (tryStatement?.catchClause && containsAwaitedEffect(tryStatement.catchClause.block.statements, effectApis))) {
|
|
2081
|
-
warnings.push({ message: `Unextractable handler ${component}.${attr}`, ...lineAndColumn(source, awaitStatement) });
|
|
2082
|
-
return [];
|
|
2083
|
-
}
|
|
2084
|
-
const successSummaries = summarizeAsyncSegment(successStatements, setters);
|
|
2085
|
-
const catchSummaries = tryStatement?.catchClause ? summarizeAsyncSegment(tryStatement.catchClause.block.statements, setters) : [];
|
|
2086
|
-
const preEffects = preSummaries.map((summary) => summary.effect);
|
|
2087
|
-
const finallySummaries = tryStatement?.finallyBlock ? summarizeAsyncSegment(tryStatement.finallyBlock.statements, setters) : [];
|
|
2088
|
-
const finallyEffects = finallySummaries.map((summary) => summary.effect);
|
|
2089
|
-
const successEffects = [...successSummaries.map((summary) => summary.effect), ...finallyEffects];
|
|
2090
|
-
const catchEffects = [...catchSummaries.map((summary) => summary.effect), ...finallyEffects];
|
|
2091
|
-
const preReads = uniqueStrings([...preSummaries.flatMap((summary) => summary.reads), ...opArgs.reads]);
|
|
2092
|
-
const successReads = uniqueStrings([...successSummaries.flatMap((summary) => summary.reads), ...finallySummaries.flatMap((summary) => summary.reads)]);
|
|
2093
|
-
const catchReads = uniqueStrings([...catchSummaries.flatMap((summary) => summary.reads), ...finallySummaries.flatMap((summary) => summary.reads)]);
|
|
2094
|
-
if (successEffects.length === 0 && catchEffects.length === 0)
|
|
2095
|
-
return [];
|
|
2096
|
-
const writes = uniqueStrings([...preEffects, ...successEffects, ...catchEffects].flatMap(effectWriteVars));
|
|
2097
|
-
const baseId = `${component}.${attr}.${op}`;
|
|
2098
|
-
for (const read of uniqueStrings([...successReads, ...catchReads])) {
|
|
2099
|
-
warnings.push({ message: `Stale-read risk ${baseId}:${read}`, ...lineAndColumn(source, awaitStatement) });
|
|
2100
|
-
}
|
|
2101
|
-
const sourceAnchor = [{ file: fileName, ...lineAndColumn(source, expression) }];
|
|
2102
|
-
const enqueue = {
|
|
2103
|
-
id: `${baseId}.start`,
|
|
2104
|
-
cls: "user",
|
|
2105
|
-
label: labelForEvent(attr, locator),
|
|
2106
|
-
source: sourceAnchor,
|
|
2107
|
-
guard: { kind: "lit", value: true },
|
|
2108
|
-
effect: { kind: "seq", effects: [...preEffects, { kind: "enqueue", op, continuation: `${baseId}.cont`, args: opArgs.args }] },
|
|
2109
|
-
reads: preReads,
|
|
2110
|
-
writes: uniqueStrings([...preEffects.flatMap(effectWriteVars), "sys:pending"]),
|
|
2111
|
-
confidence: confidenceForEffects(preEffects)
|
|
2112
|
-
};
|
|
2113
|
-
const success = {
|
|
2114
|
-
id: `${baseId}.success`,
|
|
2115
|
-
cls: "env",
|
|
2116
|
-
label: { kind: "resolve", op, outcome: "success" },
|
|
2117
|
-
source: sourceAnchor,
|
|
2118
|
-
guard: pendingIs(op),
|
|
2119
|
-
effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...successEffects] },
|
|
2120
|
-
reads: uniqueStrings(["sys:pending", ...successReads]),
|
|
2121
|
-
writes: [...new Set(["sys:pending", ...successEffects.flatMap(effectWriteVars)])],
|
|
2122
|
-
confidence: confidenceForEffects(successEffects)
|
|
2123
|
-
};
|
|
2124
|
-
const successNavigate = firstNavigationInStatements(successStatements, routePatterns);
|
|
2125
|
-
const transitions = [enqueue, successNavigate ? appendEffect(success, navigationEffect(successNavigate)) : success];
|
|
2126
|
-
if (catchEffects.length > 0 || asyncOutcomes[op]?.error !== undefined) {
|
|
2127
|
-
const errorTransition = {
|
|
2128
|
-
id: `${baseId}.error`,
|
|
2129
|
-
cls: "env",
|
|
2130
|
-
label: { kind: "resolve", op, outcome: "error" },
|
|
2131
|
-
source: sourceAnchor,
|
|
2132
|
-
guard: pendingIs(op),
|
|
2133
|
-
effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...catchEffects] },
|
|
2134
|
-
reads: uniqueStrings(["sys:pending", ...catchReads]),
|
|
2135
|
-
writes: [...new Set(["sys:pending", ...catchEffects.flatMap(effectWriteVars)])],
|
|
2136
|
-
confidence: confidenceForEffects(catchEffects)
|
|
2137
|
-
};
|
|
2138
|
-
transitions.push(errorTransition);
|
|
2139
|
-
}
|
|
2140
|
-
else {
|
|
2141
|
-
warnings.push({ message: `Unhandled rejection ${baseId}`, ...lineAndColumn(source, awaitStatement) });
|
|
2142
|
-
}
|
|
2143
|
-
return transitions.map((transition) => ({ ...transition, writes: [...new Set(transition.writes)] }));
|
|
2144
|
-
}
|
|
2145
|
-
function transitionsFromSequentialAwait(source, fileName, attr, expression, firstAwait, firstOp, preSummaries, successStatements, setters, effectApis, component, locator, warnings) {
|
|
2146
|
-
const secondIndex = successStatements.findIndex((statement) => expressionStatementAwait(statement, effectApis));
|
|
2147
|
-
if (secondIndex < 0)
|
|
2148
|
-
return [];
|
|
2149
|
-
const secondAwait = successStatements[secondIndex];
|
|
2150
|
-
const secondOp = awaitedOp(secondAwait, effectApis);
|
|
2151
|
-
const promiseAllOps = secondOp ? undefined : promiseAllAwaitOps(secondAwait, effectApis);
|
|
2152
|
-
if (!secondOp && !promiseAllOps)
|
|
2153
|
-
return [];
|
|
2154
|
-
const betweenStatements = successStatements.slice(0, secondIndex);
|
|
2155
|
-
const tailStatements = successStatements.slice(secondIndex + 1);
|
|
2156
|
-
if (containsAwaitedEffect(tailStatements, effectApis))
|
|
2157
|
-
return [];
|
|
2158
|
-
const betweenSummaries = summarizeAsyncSegment(betweenStatements, setters);
|
|
2159
|
-
const tailSummaries = summarizeAsyncSegment(tailStatements, setters);
|
|
2160
|
-
const preEffects = preSummaries.map((summary) => summary.effect);
|
|
2161
|
-
const betweenEffects = betweenSummaries.map((summary) => summary.effect);
|
|
2162
|
-
const tailEffects = tailSummaries.map((summary) => summary.effect);
|
|
2163
|
-
if (tailEffects.length === 0)
|
|
2164
|
-
return [];
|
|
2165
|
-
const preReads = uniqueStrings(preSummaries.flatMap((summary) => summary.reads));
|
|
2166
|
-
const betweenReads = uniqueStrings(betweenSummaries.flatMap((summary) => summary.reads));
|
|
2167
|
-
const tailReads = uniqueStrings(tailSummaries.flatMap((summary) => summary.reads));
|
|
2168
|
-
const firstBaseId = `${component}.${attr}.${firstOp}`;
|
|
2169
|
-
const secondBaseId = `${component}.${attr}.${secondOp ?? "Promise_all"}`;
|
|
2170
|
-
for (const read of uniqueStrings([...betweenReads, ...tailReads])) {
|
|
2171
|
-
warnings.push({ message: `Stale-read risk ${firstBaseId}:${read}`, ...lineAndColumn(source, firstAwait) });
|
|
2172
|
-
}
|
|
2173
|
-
warnings.push({ message: `Unhandled rejection ${firstBaseId}`, ...lineAndColumn(source, firstAwait) });
|
|
2174
|
-
for (const op of promiseAllOps ?? [secondOp]) {
|
|
2175
|
-
warnings.push({ message: `Unhandled rejection ${component}.${attr}.${op}`, ...lineAndColumn(source, secondAwait) });
|
|
2176
|
-
}
|
|
2177
|
-
const sourceAnchor = [{ file: fileName, ...lineAndColumn(source, expression) }];
|
|
2178
|
-
const secondEnqueueEffects = promiseAllOps
|
|
2179
|
-
? promiseAllOps.map((op) => ({ kind: "enqueue", op, continuation: `${secondBaseId}.cont`, args: {} }))
|
|
2180
|
-
: [{ kind: "enqueue", op: secondOp, continuation: `${secondBaseId}.cont`, args: {} }];
|
|
2181
|
-
const secondSuccess = promiseAllOps
|
|
2182
|
-
? {
|
|
2183
|
-
id: `${secondBaseId}.success`,
|
|
2184
|
-
cls: "env",
|
|
2185
|
-
label: { kind: "internal", text: `${secondBaseId}.join` },
|
|
2186
|
-
source: sourceAnchor,
|
|
2187
|
-
guard: promiseAllGuard(promiseAllOps),
|
|
2188
|
-
effect: { kind: "seq", effects: [...promiseAllOps.map((_, index) => ({ kind: "dequeue", index })).reverse(), ...tailEffects] },
|
|
2189
|
-
reads: uniqueStrings(["sys:pending", ...tailReads]),
|
|
2190
|
-
writes: uniqueStrings(["sys:pending", ...tailEffects.flatMap(effectWriteVars)]),
|
|
2191
|
-
confidence: confidenceForEffects(tailEffects)
|
|
2192
|
-
}
|
|
2193
|
-
: {
|
|
2194
|
-
id: `${secondBaseId}.success`,
|
|
2195
|
-
cls: "env",
|
|
2196
|
-
label: { kind: "resolve", op: secondOp, outcome: "success" },
|
|
2197
|
-
source: sourceAnchor,
|
|
2198
|
-
guard: pendingIs(secondOp),
|
|
2199
|
-
effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...tailEffects] },
|
|
2200
|
-
reads: uniqueStrings(["sys:pending", ...tailReads]),
|
|
2201
|
-
writes: uniqueStrings(["sys:pending", ...tailEffects.flatMap(effectWriteVars)]),
|
|
2202
|
-
confidence: confidenceForEffects(tailEffects)
|
|
2203
|
-
};
|
|
2204
|
-
const transitions = [
|
|
2205
|
-
{
|
|
2206
|
-
id: `${firstBaseId}.start`,
|
|
2207
|
-
cls: "user",
|
|
2208
|
-
label: labelForEvent(attr, locator),
|
|
2209
|
-
source: sourceAnchor,
|
|
2210
|
-
guard: { kind: "lit", value: true },
|
|
2211
|
-
effect: { kind: "seq", effects: [...preEffects, { kind: "enqueue", op: firstOp, continuation: `${firstBaseId}.cont`, args: {} }] },
|
|
2212
|
-
reads: preReads,
|
|
2213
|
-
writes: uniqueStrings([...preEffects.flatMap(effectWriteVars), "sys:pending"]),
|
|
2214
|
-
confidence: confidenceForEffects(preEffects)
|
|
2215
|
-
},
|
|
2216
|
-
{
|
|
2217
|
-
id: `${firstBaseId}.success`,
|
|
2218
|
-
cls: "env",
|
|
2219
|
-
label: { kind: "resolve", op: firstOp, outcome: "success" },
|
|
2220
|
-
source: sourceAnchor,
|
|
2221
|
-
guard: pendingIs(firstOp),
|
|
2222
|
-
effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...betweenEffects, ...secondEnqueueEffects] },
|
|
2223
|
-
reads: uniqueStrings(["sys:pending", ...betweenReads]),
|
|
2224
|
-
writes: uniqueStrings(["sys:pending", ...betweenEffects.flatMap(effectWriteVars)]),
|
|
2225
|
-
confidence: confidenceForEffects(betweenEffects)
|
|
2226
|
-
},
|
|
2227
|
-
secondSuccess
|
|
2228
|
-
];
|
|
2229
|
-
return transitions.map((transition) => ({ ...transition, writes: uniqueStrings(transition.writes) }));
|
|
2230
|
-
}
|
|
2231
|
-
function promiseAllGuard(ops) {
|
|
2232
|
-
return ops
|
|
2233
|
-
.map((op, index) => pendingIsAt(index, op))
|
|
2234
|
-
.reduce(andGuard);
|
|
2235
|
-
}
|
|
2236
|
-
function confidenceForEffects(effects) {
|
|
2237
|
-
return effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact";
|
|
2238
|
-
}
|
|
2239
|
-
function summarizeAsyncSegment(statements, setters) {
|
|
2240
|
-
const summaries = [];
|
|
2241
|
-
for (const statement of statements) {
|
|
2242
|
-
const summary = summarizeSetterStatement(statement, setters);
|
|
2243
|
-
if (summary) {
|
|
2244
|
-
summaries.push(summary);
|
|
2245
|
-
continue;
|
|
2246
|
-
}
|
|
2247
|
-
for (const setter of escapedSettersInStatement(statement, setters)) {
|
|
2248
|
-
summaries.push({ effect: { kind: "havoc", var: setter.varId }, reads: [] });
|
|
2249
|
-
}
|
|
2250
|
-
for (const setter of settersWrittenIn(statement, setters)) {
|
|
2251
|
-
summaries.push({ effect: { kind: "havoc", var: setter.varId }, reads: [] });
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
return uniqueSummariesByEffect(summaries);
|
|
2255
|
-
}
|
|
2256
|
-
function escapedSettersInStatement(statement, setters) {
|
|
2257
|
-
const found = [];
|
|
2258
|
-
const visit = (candidate) => {
|
|
2259
|
-
if (ts.isCallExpression(candidate))
|
|
2260
|
-
found.push(...escapedSetters(candidate, setters));
|
|
2261
|
-
ts.forEachChild(candidate, visit);
|
|
2262
|
-
};
|
|
2263
|
-
visit(statement);
|
|
2264
|
-
return uniqueSetters(found);
|
|
2265
|
-
}
|
|
2266
|
-
function uniqueSummariesByEffect(summaries) {
|
|
2267
|
-
const seen = new Set();
|
|
2268
|
-
const out = [];
|
|
2269
|
-
for (const summary of summaries) {
|
|
2270
|
-
const key = JSON.stringify(summary.effect);
|
|
2271
|
-
if (seen.has(key))
|
|
2272
|
-
continue;
|
|
2273
|
-
seen.add(key);
|
|
2274
|
-
out.push(summary);
|
|
2275
|
-
}
|
|
2276
|
-
return out;
|
|
2277
|
-
}
|
|
2278
|
-
function summarizeSetterStatement(statement, setters, locals = new Map()) {
|
|
2279
|
-
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
|
|
2280
|
-
return undefined;
|
|
2281
|
-
return summarizeSetterCall(statement.expression, setters, locals);
|
|
2282
|
-
}
|
|
2283
|
-
function summarizeSetterCall(call, setters, locals = new Map()) {
|
|
2284
|
-
const setterCall = setterCallFrom(call, setters);
|
|
2285
|
-
if (!setterCall)
|
|
2286
|
-
return undefined;
|
|
2287
|
-
const assignment = setterArgumentExpr(setterCall.argument, setterCall.setter, setters, locals);
|
|
2288
|
-
if (!assignment) {
|
|
2289
|
-
return {
|
|
2290
|
-
effect: { kind: "havoc", var: setterCall.setter.varId },
|
|
2291
|
-
reads: []
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
return {
|
|
2295
|
-
effect: { kind: "assign", var: setterCall.setter.varId, expr: assignment.expr },
|
|
2296
|
-
reads: assignment.reads
|
|
2297
|
-
};
|
|
2298
|
-
}
|
|
2299
|
-
function transitionsFromUseEffect(source, fileName, node, setters, component) {
|
|
2300
|
-
const callback = node.arguments[0];
|
|
2301
|
-
if (!callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body))
|
|
2302
|
-
return [];
|
|
2303
|
-
const cleanup = cleanupSummaries(callback.body.statements, setters);
|
|
2304
|
-
const bodyStatements = callback.body.statements.filter((statement) => !isCleanupReturn(statement));
|
|
2305
|
-
const summaries = summarizeEffectStatements(bodyStatements, setters);
|
|
2306
|
-
if (!summaries || !cleanup)
|
|
2307
|
-
return [];
|
|
2308
|
-
const transitions = [];
|
|
2309
|
-
const effectReads = uniqueStrings(summaries.flatMap((summary) => summary.reads));
|
|
2310
|
-
const deps = dependencyReads(node.arguments[1], setters, effectReads);
|
|
2311
|
-
const effects = summaries.map((summary) => summary.effect);
|
|
2312
|
-
if (effects.length > 0) {
|
|
2313
|
-
const assignEffects = effects.filter((effect) => effect.kind === "assign");
|
|
2314
|
-
const guards = assignEffects.map((effect) => ({ kind: "neq", args: [{ kind: "read", var: effect.var }, effect.expr] }));
|
|
2315
|
-
const guard = guards.length > 0 ? guards.slice(1).reduce((acc, next) => andGuard(acc, next), guards[0]) : { kind: "lit", value: true };
|
|
2316
|
-
transitions.push({
|
|
2317
|
-
id: `${component}.useEffect.${effects.flatMap(effectWriteVars).map((varId) => varId.split(".").at(-1) ?? varId).join("_")}`,
|
|
2318
|
-
cls: "internal",
|
|
2319
|
-
label: { kind: "internal", text: `${component}.useEffect` },
|
|
2320
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
2321
|
-
guard,
|
|
2322
|
-
effect: effects.length === 1 ? effects[0] : { kind: "seq", effects },
|
|
2323
|
-
reads: uniqueStrings([...deps, ...effectReads, ...effects.flatMap(effectWriteVars)]),
|
|
2324
|
-
writes: uniqueStrings(effects.flatMap(effectWriteVars)),
|
|
2325
|
-
confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact",
|
|
2326
|
-
triggeredBy: deps
|
|
2327
|
-
});
|
|
2328
|
-
}
|
|
2329
|
-
if (cleanup.length > 0) {
|
|
2330
|
-
const cleanupEffects = cleanup.map((summary) => summary.effect);
|
|
2331
|
-
const cleanupReads = uniqueStrings(cleanup.flatMap((summary) => summary.reads));
|
|
2332
|
-
transitions.push({
|
|
2333
|
-
id: `${component}.useEffect.cleanup.${cleanupEffects.flatMap(effectWriteVars).map((varId) => varId.split(".").at(-1) ?? varId).join("_")}`,
|
|
2334
|
-
cls: "internal",
|
|
2335
|
-
label: { kind: "internal", text: `${component}.useEffect.cleanup` },
|
|
2336
|
-
source: [{ file: fileName, ...lineAndColumn(source, node) }],
|
|
2337
|
-
guard: { kind: "lit", value: true },
|
|
2338
|
-
effect: cleanupEffects.length === 1 ? cleanupEffects[0] : { kind: "seq", effects: cleanupEffects },
|
|
2339
|
-
reads: cleanupReads,
|
|
2340
|
-
writes: uniqueStrings(cleanupEffects.flatMap(effectWriteVars)),
|
|
2341
|
-
confidence: "over-approx",
|
|
2342
|
-
triggeredBy: deps
|
|
2343
|
-
});
|
|
2344
|
-
}
|
|
2345
|
-
return transitions;
|
|
2346
|
-
}
|
|
2347
|
-
function summarizeEffectStatements(statements, setters) {
|
|
2348
|
-
const locals = new Map();
|
|
2349
|
-
const summaries = [];
|
|
2350
|
-
for (const statement of statements) {
|
|
2351
|
-
if (bindConstStatement(statement, setters, locals))
|
|
2352
|
-
continue;
|
|
2353
|
-
const summary = summarizeSetterStatement(statement, setters, locals);
|
|
2354
|
-
if (!summary)
|
|
2355
|
-
return undefined;
|
|
2356
|
-
summaries.push(summary);
|
|
2357
|
-
}
|
|
2358
|
-
return summaries;
|
|
2359
|
-
}
|
|
2360
|
-
function cleanupSummaries(statements, setters) {
|
|
2361
|
-
const returns = statements.filter(isCleanupReturn);
|
|
2362
|
-
if (returns.length === 0)
|
|
2363
|
-
return [];
|
|
2364
|
-
if (returns.length > 1)
|
|
2365
|
-
return undefined;
|
|
2366
|
-
const expression = returns[0].expression;
|
|
2367
|
-
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression)) || !ts.isBlock(expression.body))
|
|
2368
|
-
return undefined;
|
|
2369
|
-
return summarizeEffectStatements(expression.body.statements, setters);
|
|
2370
|
-
}
|
|
2371
|
-
function isCleanupReturn(statement) {
|
|
2372
|
-
if (!ts.isReturnStatement(statement) || !statement.expression)
|
|
2373
|
-
return false;
|
|
2374
|
-
return ts.isArrowFunction(statement.expression) || ts.isFunctionExpression(statement.expression);
|
|
2375
|
-
}
|
|
2376
|
-
function dependencyReads(node, setters, fallbackReads = []) {
|
|
2377
|
-
if (!node || !ts.isArrayLiteralExpression(node)) {
|
|
2378
|
-
return uniqueStrings(fallbackReads);
|
|
2379
|
-
}
|
|
2380
|
-
return [...new Set(node.elements.flatMap((element) => ts.isIdentifier(element) ? [stateVarForName(element.text, setters)].filter((id) => Boolean(id)) : []))];
|
|
2381
|
-
}
|
|
2382
|
-
function useEffectWritesModeledState(node, setters) {
|
|
2383
|
-
let writes = false;
|
|
2384
|
-
const visit = (candidate) => {
|
|
2385
|
-
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && setters.has(candidate.expression.text))
|
|
2386
|
-
writes = true;
|
|
2387
|
-
ts.forEachChild(candidate, visit);
|
|
2388
|
-
};
|
|
2389
|
-
visit(node);
|
|
2390
|
-
return writes;
|
|
2391
|
-
}
|
|
2392
|
-
function applyParsedGuard(transitions, parsed) {
|
|
2393
|
-
if (!parsed)
|
|
2394
|
-
return transitions;
|
|
2395
|
-
return transitions.map((transition) => transition.cls === "user"
|
|
2396
|
-
? {
|
|
2397
|
-
...transition,
|
|
2398
|
-
guard: andGuard(parsed.expr, transition.guard),
|
|
2399
|
-
reads: [...new Set([...transition.reads, ...parsed.reads])]
|
|
2400
|
-
}
|
|
2401
|
-
: transition);
|
|
2402
|
-
}
|
|
2403
|
-
function combineParsedGuards(guards) {
|
|
2404
|
-
const parsed = guards.filter((guard) => Boolean(guard));
|
|
2405
|
-
if (parsed.length === 0)
|
|
2406
|
-
return undefined;
|
|
2407
|
-
return {
|
|
2408
|
-
expr: parsed.map((guard) => guard.expr).reduce(andGuard),
|
|
2409
|
-
reads: [...new Set(parsed.flatMap((guard) => guard.reads))]
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
function renderGuardFor(eventAttribute, setters, warnings, source, component, locals = new Map()) {
|
|
2413
|
-
const element = jsxElementForAttribute(eventAttribute);
|
|
2414
|
-
if (!element)
|
|
2415
|
-
return undefined;
|
|
2416
|
-
const guards = [];
|
|
2417
|
-
let current = element;
|
|
2418
|
-
while (current.parent) {
|
|
2419
|
-
const parent = current.parent;
|
|
2420
|
-
if (ts.isBinaryExpression(parent) &&
|
|
2421
|
-
parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
2422
|
-
parent.right === current) {
|
|
2423
|
-
const parsed = parseConjunctiveGuardExpression(parent.left, setters, locals);
|
|
2424
|
-
if (!parsed) {
|
|
2425
|
-
warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.left) });
|
|
2426
|
-
return undefined;
|
|
2427
|
-
}
|
|
2428
|
-
guards.push(parsed);
|
|
2429
|
-
current = parent;
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
if (ts.isConditionalExpression(parent) && parent.whenTrue === current) {
|
|
2433
|
-
const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
|
|
2434
|
-
if (!parsed) {
|
|
2435
|
-
warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
|
|
2436
|
-
return undefined;
|
|
2437
|
-
}
|
|
2438
|
-
guards.push(parsed);
|
|
2439
|
-
current = parent;
|
|
2440
|
-
continue;
|
|
2441
|
-
}
|
|
2442
|
-
if (ts.isConditionalExpression(parent) && parent.whenFalse === current) {
|
|
2443
|
-
const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
|
|
2444
|
-
if (!parsed) {
|
|
2445
|
-
warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
|
|
2446
|
-
return undefined;
|
|
2447
|
-
}
|
|
2448
|
-
guards.push({ expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads });
|
|
2449
|
-
current = parent;
|
|
2450
|
-
continue;
|
|
2451
|
-
}
|
|
2452
|
-
if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent) || ts.isJsxElement(parent) || ts.isJsxFragment(parent)) {
|
|
2453
|
-
current = parent;
|
|
2454
|
-
continue;
|
|
2455
|
-
}
|
|
2456
|
-
return combineParsedGuards(guards);
|
|
2457
|
-
}
|
|
2458
|
-
return combineParsedGuards(guards);
|
|
2459
|
-
}
|
|
2460
|
-
function jsxElementForAttribute(attribute) {
|
|
2461
|
-
const attrs = attribute.parent;
|
|
2462
|
-
if (!ts.isJsxAttributes(attrs))
|
|
2463
|
-
return undefined;
|
|
2464
|
-
const element = attrs.parent;
|
|
2465
|
-
if (ts.isJsxOpeningElement(element) && ts.isJsxElement(element.parent))
|
|
2466
|
-
return element.parent;
|
|
2467
|
-
return ts.isJsxSelfClosingElement(element) ? element : undefined;
|
|
2468
|
-
}
|
|
2469
|
-
function disabledGuardFor(eventAttribute, setters, warnings, source, component, locals = new Map()) {
|
|
2470
|
-
const attrs = eventAttribute.parent;
|
|
2471
|
-
if (!ts.isJsxAttributes(attrs))
|
|
2472
|
-
return undefined;
|
|
2473
|
-
const disabled = attrs.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && (property.name.text === "disabled" || property.name.text === "aria-disabled")) ?? submitButtonDisabledAttribute(eventAttribute);
|
|
2474
|
-
if (!disabled)
|
|
2475
|
-
return undefined;
|
|
2476
|
-
const parsed = jsxAttributeBoolean(disabled, setters, locals);
|
|
2477
|
-
if (!parsed) {
|
|
2478
|
-
warnings.push({ message: `Unsupported disabled guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, disabled) });
|
|
2479
|
-
return undefined;
|
|
2480
|
-
}
|
|
2481
|
-
return { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads };
|
|
2482
|
-
}
|
|
2483
|
-
function submitButtonDisabledAttribute(eventAttribute) {
|
|
2484
|
-
if (!ts.isIdentifier(eventAttribute.name) || eventAttribute.name.text !== "onSubmit")
|
|
2485
|
-
return undefined;
|
|
2486
|
-
const element = jsxElementForAttribute(eventAttribute);
|
|
2487
|
-
if (!element || !ts.isJsxElement(element))
|
|
2488
|
-
return undefined;
|
|
2489
|
-
let found;
|
|
2490
|
-
const visit = (node) => {
|
|
2491
|
-
if (found)
|
|
2492
|
-
return;
|
|
2493
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
2494
|
-
const tag = ts.isIdentifier(node.tagName) ? node.tagName.text : undefined;
|
|
2495
|
-
if (tag === "button" && stringAttribute(node.attributes, "type") === "submit") {
|
|
2496
|
-
found = node.attributes.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && (property.name.text === "disabled" || property.name.text === "aria-disabled"));
|
|
2497
|
-
if (found)
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
ts.forEachChild(node, visit);
|
|
2502
|
-
};
|
|
2503
|
-
visit(element);
|
|
2504
|
-
return found;
|
|
2505
|
-
}
|
|
2506
|
-
function jsxAttributeBoolean(attribute, setters, locals = new Map()) {
|
|
2507
|
-
if (!attribute.initializer)
|
|
2508
|
-
return { expr: { kind: "lit", value: true }, reads: [] };
|
|
2509
|
-
if (ts.isStringLiteral(attribute.initializer))
|
|
2510
|
-
return { expr: { kind: "lit", value: attribute.initializer.text === "true" }, reads: [] };
|
|
2511
|
-
if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression)
|
|
2512
|
-
return undefined;
|
|
2513
|
-
return parseConjunctiveGuardExpression(attribute.initializer.expression, setters, locals);
|
|
2514
|
-
}
|
|
2515
|
-
function parseGuardExpression(expression, setters, locals = new Map()) {
|
|
2516
|
-
if (expression.kind === ts.SyntaxKind.TrueKeyword)
|
|
2517
|
-
return { expr: { kind: "lit", value: true }, reads: [] };
|
|
2518
|
-
if (expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
2519
|
-
return { expr: { kind: "lit", value: false }, reads: [] };
|
|
2520
|
-
if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
|
|
2521
|
-
return valueExpr(expression, setters, locals);
|
|
2522
|
-
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2523
|
-
const parsed = parseGuardExpression(expression.operand, setters, locals);
|
|
2524
|
-
return parsed ? { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads } : undefined;
|
|
2525
|
-
}
|
|
2526
|
-
if (ts.isParenthesizedExpression(expression))
|
|
2527
|
-
return parseGuardExpression(expression.expression, setters, locals);
|
|
2528
|
-
if (ts.isBinaryExpression(expression))
|
|
2529
|
-
return parseBinaryGuardExpression(expression, setters, locals);
|
|
2530
|
-
return undefined;
|
|
2531
|
-
}
|
|
2532
|
-
function parseBinaryGuardExpression(expression, setters, locals = new Map()) {
|
|
2533
|
-
if (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
2534
|
-
const left = parseGuardExpression(expression.left, setters, locals);
|
|
2535
|
-
const right = parseGuardExpression(expression.right, setters, locals);
|
|
2536
|
-
if (!left || !right)
|
|
2537
|
-
return undefined;
|
|
2538
|
-
return {
|
|
2539
|
-
expr: { kind: expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ? "and" : "or", args: [left.expr, right.expr] },
|
|
2540
|
-
reads: [...new Set([...left.reads, ...right.reads])]
|
|
2541
|
-
};
|
|
2542
|
-
}
|
|
2543
|
-
if (expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
2544
|
-
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
|
|
2545
|
-
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
2546
|
-
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
2547
|
-
const left = parseGuardOperand(expression.left, setters, locals);
|
|
2548
|
-
const right = parseGuardOperand(expression.right, setters, locals);
|
|
2549
|
-
if (!left || !right)
|
|
2550
|
-
return undefined;
|
|
2551
|
-
return {
|
|
2552
|
-
expr: {
|
|
2553
|
-
kind: expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken || expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken ? "neq" : "eq",
|
|
2554
|
-
args: [left.expr, right.expr]
|
|
2555
|
-
},
|
|
2556
|
-
reads: [...new Set([...left.reads, ...right.reads])]
|
|
2557
|
-
};
|
|
2558
|
-
}
|
|
2559
|
-
return undefined;
|
|
2560
|
-
}
|
|
2561
|
-
function parseGuardOperand(expression, setters, locals = new Map()) {
|
|
2562
|
-
const value = literalValue(expression);
|
|
2563
|
-
if (value !== undefined)
|
|
2564
|
-
return { expr: { kind: "lit", value }, reads: [] };
|
|
2565
|
-
if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
|
|
2566
|
-
return valueExpr(expression, setters, locals);
|
|
2567
|
-
return parseGuardExpression(expression, setters, locals);
|
|
2568
|
-
}
|
|
2569
|
-
function parseConjunctiveGuardExpression(expression, setters, locals = new Map()) {
|
|
2570
|
-
if (ts.isParenthesizedExpression(expression))
|
|
2571
|
-
return parseConjunctiveGuardExpression(expression.expression, setters, locals);
|
|
2572
|
-
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2573
|
-
return combineParsedGuards([
|
|
2574
|
-
parseConjunctiveGuardExpression(expression.left, setters, locals),
|
|
2575
|
-
parseConjunctiveGuardExpression(expression.right, setters, locals)
|
|
2576
|
-
]);
|
|
2577
|
-
}
|
|
2578
|
-
return parseGuardExpression(expression, setters, locals);
|
|
2579
|
-
}
|
|
2580
|
-
function stateVarForName(name, setters) {
|
|
2581
|
-
return setterForName(name, setters)?.varId;
|
|
2582
|
-
}
|
|
2583
|
-
function setterForName(name, setters) {
|
|
2584
|
-
return setters.get(name) ?? [...setters.values()].find((setter) => setter.stateName === name);
|
|
2585
|
-
}
|
|
2586
|
-
function taggedPathDomain(domain, path) {
|
|
2587
|
-
const [field, ...rest] = path;
|
|
2588
|
-
if (!field)
|
|
2589
|
-
return domain;
|
|
2590
|
-
const variants = Object.values(domain.variants).filter((variant) => variant.kind === "record");
|
|
2591
|
-
const fieldDomains = variants.map((variant) => variant.fields[field]).filter((candidate) => Boolean(candidate));
|
|
2592
|
-
if (fieldDomains.length === 0)
|
|
2593
|
-
return undefined;
|
|
2594
|
-
const first = fieldDomains[0];
|
|
2595
|
-
if (rest.length === 0)
|
|
2596
|
-
return first;
|
|
2597
|
-
return first.kind === "record" ? domainAtRecordPath(first, rest) : undefined;
|
|
2598
|
-
}
|
|
2599
|
-
function domainAtRecordPath(domain, path) {
|
|
2600
|
-
const [field, ...rest] = path;
|
|
2601
|
-
if (!field)
|
|
2602
|
-
return domain;
|
|
2603
|
-
const next = domain.fields[field];
|
|
2604
|
-
if (!next || rest.length === 0)
|
|
2605
|
-
return next;
|
|
2606
|
-
return next.kind === "record" ? domainAtRecordPath(next, rest) : undefined;
|
|
2607
|
-
}
|
|
2608
|
-
function andGuard(left, right) {
|
|
2609
|
-
if (isTrueLiteral(left))
|
|
2610
|
-
return right;
|
|
2611
|
-
if (isTrueLiteral(right))
|
|
2612
|
-
return left;
|
|
2613
|
-
return { kind: "and", args: [left, right] };
|
|
2614
|
-
}
|
|
2615
|
-
function isTrueLiteral(expr) {
|
|
2616
|
-
return expr.kind === "lit" && expr.value === true;
|
|
2617
|
-
}
|
|
2618
|
-
function isEventAttribute(name) {
|
|
2619
|
-
return name === "onClick" || name === "onSubmit" || name === "onChange" || name === "onInput";
|
|
2620
|
-
}
|
|
2621
|
-
function labelForEvent(name, locator) {
|
|
2622
|
-
if (name === "onSubmit")
|
|
2623
|
-
return { kind: "submit", ...(locator ? { locator } : {}) };
|
|
2624
|
-
if (name === "onChange" || name === "onInput")
|
|
2625
|
-
return { kind: "input", valueClass: "literal", ...(locator ? { locator } : {}) };
|
|
2626
|
-
return { kind: "click", ...(locator ? { locator } : {}) };
|
|
2627
|
-
}
|
|
2628
|
-
function locatorForEventAttribute(attribute) {
|
|
2629
|
-
const attrs = attribute.parent;
|
|
2630
|
-
if (!ts.isJsxAttributes(attrs))
|
|
2631
|
-
return undefined;
|
|
2632
|
-
const testId = stringAttribute(attrs, "data-testid");
|
|
2633
|
-
if (testId)
|
|
2634
|
-
return { kind: "testId", value: testId };
|
|
2635
|
-
const element = attrs.parent;
|
|
2636
|
-
const role = stringAttribute(attrs, "role") ?? inferredRole(element);
|
|
2637
|
-
if (!role)
|
|
2638
|
-
return undefined;
|
|
2639
|
-
const name = stringAttribute(attrs, "aria-label") ?? simpleElementText(element);
|
|
2640
|
-
return name ? { kind: "role", role, name } : { kind: "role", role };
|
|
2641
|
-
}
|
|
2642
|
-
function stringAttribute(attrs, name) {
|
|
2643
|
-
const attr = attrs.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === name);
|
|
2644
|
-
if (!attr?.initializer || !ts.isStringLiteral(attr.initializer))
|
|
2645
|
-
return undefined;
|
|
2646
|
-
return attr.initializer.text;
|
|
2647
|
-
}
|
|
2648
|
-
function inferredRole(node) {
|
|
2649
|
-
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node))
|
|
2650
|
-
return undefined;
|
|
2651
|
-
const tag = node.tagName.getText();
|
|
2652
|
-
if (tag === "button")
|
|
2653
|
-
return "button";
|
|
2654
|
-
if (tag === "form")
|
|
2655
|
-
return "form";
|
|
2656
|
-
if (tag === "input" && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))) {
|
|
2657
|
-
const type = stringAttribute(node.attributes, "type");
|
|
2658
|
-
if (type === "radio")
|
|
2659
|
-
return "radio";
|
|
2660
|
-
if (type === "checkbox")
|
|
2661
|
-
return "checkbox";
|
|
2662
|
-
return "textbox";
|
|
2663
|
-
}
|
|
2664
|
-
if (tag === "select")
|
|
2665
|
-
return "combobox";
|
|
2666
|
-
if (tag === "textarea")
|
|
2667
|
-
return "textbox";
|
|
2668
|
-
return undefined;
|
|
2669
|
-
}
|
|
2670
|
-
function simpleElementText(node) {
|
|
2671
|
-
if (!ts.isJsxOpeningElement(node) || !ts.isJsxElement(node.parent))
|
|
2672
|
-
return undefined;
|
|
2673
|
-
const text = node.parent.children
|
|
2674
|
-
.filter(ts.isJsxText)
|
|
2675
|
-
.map((child) => child.getText().replace(/\s+/g, " ").trim())
|
|
2676
|
-
.filter(Boolean)
|
|
2677
|
-
.join(" ");
|
|
2678
|
-
return text || undefined;
|
|
2679
|
-
}
|
|
2680
|
-
function isEventTargetValue(node, parameter) {
|
|
2681
|
-
if (!parameter || !ts.isIdentifier(parameter.name))
|
|
2682
|
-
return false;
|
|
2683
|
-
const path = propertyAccessPath(node);
|
|
2684
|
-
if (!path)
|
|
2685
|
-
return false;
|
|
2686
|
-
return (path.length === 3 &&
|
|
2687
|
-
path[0] === parameter.name.text &&
|
|
2688
|
-
(path[1] === "target" || path[1] === "currentTarget") &&
|
|
2689
|
-
path[2] === "value");
|
|
2690
|
-
}
|
|
2691
|
-
function isInputValueExpression(node, parameter) {
|
|
2692
|
-
if (isEventTargetValue(node, parameter))
|
|
2693
|
-
return true;
|
|
2694
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (node.expression.text === "Number" || node.expression.text === "String")) {
|
|
2695
|
-
return node.arguments.length === 1 && isInputValueExpression(node.arguments[0], parameter);
|
|
2696
|
-
}
|
|
2697
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && (node.expression.name.text === "trim" || node.expression.name.text === "toLowerCase")) {
|
|
2698
|
-
return node.arguments.length === 0 && isInputValueExpression(node.expression.expression, parameter);
|
|
2699
|
-
}
|
|
2700
|
-
return false;
|
|
2701
|
-
}
|
|
2702
|
-
function propertyAccessPath(node) {
|
|
2703
|
-
if (ts.isIdentifier(node))
|
|
2704
|
-
return [node.text];
|
|
2705
|
-
if (isPropertyAccessLike(node)) {
|
|
2706
|
-
const base = propertyAccessPath(node.expression);
|
|
2707
|
-
return base ? [...base, node.name.text] : undefined;
|
|
2708
|
-
}
|
|
2709
|
-
return undefined;
|
|
2710
|
-
}
|
|
2711
|
-
function isPropertyAccessLike(node) {
|
|
2712
|
-
return ts.isPropertyAccessExpression(node) || ts.isPropertyAccessChain(node);
|
|
2713
|
-
}
|
|
2714
|
-
function valueClassForDomain(domain) {
|
|
2715
|
-
if (domain.kind === "enum")
|
|
2716
|
-
return domain.values.join("|") || "enum";
|
|
2717
|
-
if (domain.kind === "boundedInt")
|
|
2718
|
-
return `${domain.min}..${domain.max}`;
|
|
2719
|
-
return domain.kind;
|
|
2720
|
-
}
|
|
2721
|
-
function safeId(value) {
|
|
2722
|
-
return value.replace(/[^a-zA-Z0-9_-]+/g, "_") || "value";
|
|
2723
|
-
}
|
|
2724
|
-
function uniqueStrings(values) {
|
|
2725
|
-
return [...new Set(values)].sort();
|
|
2726
|
-
}
|
|
2727
|
-
function literalValue(node) {
|
|
2728
|
-
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
2729
|
-
return true;
|
|
2730
|
-
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
2731
|
-
return false;
|
|
2732
|
-
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
2733
|
-
return null;
|
|
2734
|
-
if (ts.isStringLiteral(node))
|
|
2735
|
-
return node.text;
|
|
2736
|
-
if (ts.isNumericLiteral(node))
|
|
2737
|
-
return Number(node.text);
|
|
2738
|
-
return undefined;
|
|
2739
|
-
}
|
|
2740
|
-
function setterAssignEffect(statement, setters) {
|
|
2741
|
-
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
|
|
2742
|
-
return undefined;
|
|
2743
|
-
const call = statement.expression;
|
|
2744
|
-
if (!ts.isIdentifier(call.expression) || call.arguments.length !== 1)
|
|
2745
|
-
return undefined;
|
|
2746
|
-
const setter = setters.get(call.expression.text);
|
|
2747
|
-
const value = literalValue(call.arguments[0]);
|
|
2748
|
-
if (!setter || value === undefined)
|
|
2749
|
-
return undefined;
|
|
2750
|
-
return { kind: "assign", var: setter.varId, expr: { kind: "lit", value } };
|
|
2751
|
-
}
|
|
2752
|
-
function expressionStatementAwait(statement, effectApis) {
|
|
2753
|
-
return Boolean(awaitedOp(statement, effectApis) ?? promiseAllAwaitOps(statement, effectApis));
|
|
2754
|
-
}
|
|
2755
|
-
function containsAwaitedEffect(statements, effectApis) {
|
|
2756
|
-
return statements.some((statement) => {
|
|
2757
|
-
let found = false;
|
|
2758
|
-
const visit = (node, insideAwait = false) => {
|
|
2759
|
-
if (found)
|
|
2760
|
-
return;
|
|
2761
|
-
if (ts.isAwaitExpression(node)) {
|
|
2762
|
-
visit(node.expression, true);
|
|
2763
|
-
return;
|
|
2764
|
-
}
|
|
2765
|
-
if (insideAwait && ts.isCallExpression(node)) {
|
|
2766
|
-
const name = callName(node.expression);
|
|
2767
|
-
if (name && effectApis.has(effectOpForCall(name, node))) {
|
|
2768
|
-
found = true;
|
|
2769
|
-
return;
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
ts.forEachChild(node, (child) => visit(child, insideAwait));
|
|
2773
|
-
};
|
|
2774
|
-
visit(statement);
|
|
2775
|
-
return found;
|
|
2776
|
-
});
|
|
2777
|
-
}
|
|
2778
|
-
function awaitedOp(statement, effectApis) {
|
|
2779
|
-
return awaitedCall(statement, effectApis)?.op;
|
|
2780
|
-
}
|
|
2781
|
-
function awaitedCall(statement, effectApis) {
|
|
2782
|
-
const awaitExpression = awaitedCallExpressionInStatement(statement);
|
|
2783
|
-
if (!awaitExpression)
|
|
2784
|
-
return undefined;
|
|
2785
|
-
const name = callName(awaitExpression.expression);
|
|
2786
|
-
if (!name)
|
|
2787
|
-
return undefined;
|
|
2788
|
-
const op = effectOpForCall(name, awaitExpression);
|
|
2789
|
-
if (!effectApis.has(op))
|
|
2790
|
-
return undefined;
|
|
2791
|
-
return { op, call: awaitExpression };
|
|
2792
|
-
}
|
|
2793
|
-
function awaitedCallExpressionInStatement(statement) {
|
|
2794
|
-
if (ts.isExpressionStatement(statement) && ts.isAwaitExpression(statement.expression) && ts.isCallExpression(statement.expression.expression)) {
|
|
2795
|
-
return statement.expression.expression;
|
|
2796
|
-
}
|
|
2797
|
-
if (ts.isVariableStatement(statement)) {
|
|
2798
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
2799
|
-
if (declaration.initializer &&
|
|
2800
|
-
ts.isAwaitExpression(declaration.initializer) &&
|
|
2801
|
-
ts.isCallExpression(declaration.initializer.expression) &&
|
|
2802
|
-
callName(declaration.initializer.expression.expression) === "fetch") {
|
|
2803
|
-
return declaration.initializer.expression;
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
return undefined;
|
|
2808
|
-
}
|
|
2809
|
-
function effectOpForCall(name, call) {
|
|
2810
|
-
if (name !== "fetch")
|
|
2811
|
-
return name;
|
|
2812
|
-
const url = fetchUrl(call.arguments[0]);
|
|
2813
|
-
const method = fetchMethod(call.arguments[1]) ?? "GET";
|
|
2814
|
-
return url ? `${method} ${url}` : "fetch";
|
|
2815
|
-
}
|
|
2816
|
-
function fetchUrl(argument) {
|
|
2817
|
-
if (!argument)
|
|
2818
|
-
return undefined;
|
|
2819
|
-
if (ts.isStringLiteral(argument) || ts.isNoSubstitutionTemplateLiteral(argument))
|
|
2820
|
-
return normalizeFetchUrl(argument.text);
|
|
2821
|
-
if (ts.isTemplateExpression(argument)) {
|
|
2822
|
-
const pattern = templateRoutePattern(argument);
|
|
2823
|
-
return pattern ? normalizeFetchUrl(pattern.replace(/\/:param(?=\/|$)/g, "/:id")) : undefined;
|
|
2824
|
-
}
|
|
2825
|
-
return undefined;
|
|
2826
|
-
}
|
|
2827
|
-
function normalizeFetchUrl(value) {
|
|
2828
|
-
return value.startsWith("/") ? value : `/${value}`;
|
|
2829
|
-
}
|
|
2830
|
-
function fetchMethod(argument) {
|
|
2831
|
-
if (!argument || !ts.isObjectLiteralExpression(argument))
|
|
2832
|
-
return undefined;
|
|
2833
|
-
const method = argument.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property.name) === "method");
|
|
2834
|
-
const value = method ? literalValue(method.initializer) : undefined;
|
|
2835
|
-
return typeof value === "string" ? value.toUpperCase() : undefined;
|
|
2836
|
-
}
|
|
2837
|
-
function firstNavigationInStatements(statements, routePatterns) {
|
|
2838
|
-
for (const statement of statements) {
|
|
2839
|
-
let found;
|
|
2840
|
-
const visit = (node) => {
|
|
2841
|
-
if (found)
|
|
2842
|
-
return;
|
|
2843
|
-
if (ts.isCallExpression(node))
|
|
2844
|
-
found = navigationCall(node, undefined, routePatterns);
|
|
2845
|
-
ts.forEachChild(node, visit);
|
|
2846
|
-
};
|
|
2847
|
-
visit(statement);
|
|
2848
|
-
if (found)
|
|
2849
|
-
return found;
|
|
2850
|
-
}
|
|
2851
|
-
return undefined;
|
|
2852
|
-
}
|
|
2853
|
-
function navigationEffect(navigation) {
|
|
2854
|
-
return {
|
|
2855
|
-
kind: "navigate",
|
|
2856
|
-
mode: navigation.mode,
|
|
2857
|
-
...(navigation.to ? { to: { kind: "lit", value: navigation.to } } : {})
|
|
2858
|
-
};
|
|
2859
|
-
}
|
|
2860
|
-
function appendEffect(transition, effect) {
|
|
2861
|
-
const current = transition.effect.kind === "seq" ? transition.effect.effects : [transition.effect];
|
|
2862
|
-
const writes = uniqueStrings([...transition.writes, ...effectWriteVars(effect)]);
|
|
2863
|
-
const reads = uniqueStrings([...transition.reads, ...(effect.kind === "navigate" ? ["sys:route", "sys:history"] : [])]);
|
|
2864
|
-
return {
|
|
2865
|
-
...transition,
|
|
2866
|
-
effect: { kind: "seq", effects: [...current, effect] },
|
|
2867
|
-
reads,
|
|
2868
|
-
writes
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
2871
|
-
function effectCallArgs(call, setters, locals) {
|
|
2872
|
-
const first = call.arguments[0];
|
|
2873
|
-
if (!first)
|
|
2874
|
-
return { args: {}, reads: [] };
|
|
2875
|
-
if (ts.isObjectLiteralExpression(first)) {
|
|
2876
|
-
const args = {};
|
|
2877
|
-
const reads = new Set();
|
|
2878
|
-
for (const property of first.properties) {
|
|
2879
|
-
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property))
|
|
2880
|
-
return { args: {}, reads: [] };
|
|
2881
|
-
const name = propertyName(property.name);
|
|
2882
|
-
if (!name)
|
|
2883
|
-
return { args: {}, reads: [] };
|
|
2884
|
-
const value = ts.isShorthandPropertyAssignment(property) ? valueExpr(property.name, setters, locals) : valueExpr(property.initializer, setters, locals);
|
|
2885
|
-
if (!value)
|
|
2886
|
-
return { args: {}, reads: [] };
|
|
2887
|
-
args[name] = value.expr;
|
|
2888
|
-
value.reads.forEach((read) => reads.add(read));
|
|
2889
|
-
}
|
|
2890
|
-
return { args, reads: [...reads] };
|
|
2891
|
-
}
|
|
2892
|
-
const value = valueExpr(first, setters, locals);
|
|
2893
|
-
return value ? { args: { value: value.expr }, reads: value.reads } : { args: {}, reads: [] };
|
|
2894
|
-
}
|
|
2895
|
-
function promiseAllAwaitOps(statement, effectApis) {
|
|
2896
|
-
if (!ts.isExpressionStatement(statement))
|
|
2897
|
-
return undefined;
|
|
2898
|
-
const expression = statement.expression;
|
|
2899
|
-
if (!ts.isAwaitExpression(expression) || !ts.isCallExpression(expression.expression))
|
|
2900
|
-
return undefined;
|
|
2901
|
-
const call = expression.expression;
|
|
2902
|
-
if (callName(call.expression) !== "Promise.all" || call.arguments.length !== 1 || !ts.isArrayLiteralExpression(call.arguments[0]))
|
|
2903
|
-
return undefined;
|
|
2904
|
-
const ops = [];
|
|
2905
|
-
for (const element of call.arguments[0].elements) {
|
|
2906
|
-
if (!ts.isCallExpression(element))
|
|
2907
|
-
return undefined;
|
|
2908
|
-
const name = callName(element.expression);
|
|
2909
|
-
if (!name || !effectApis.has(name))
|
|
2910
|
-
return undefined;
|
|
2911
|
-
ops.push(name);
|
|
2912
|
-
}
|
|
2913
|
-
return ops.length > 0 ? ops : undefined;
|
|
2914
|
-
}
|
|
2915
|
-
function callName(expression) {
|
|
2916
|
-
if (ts.isIdentifier(expression))
|
|
2917
|
-
return expression.text;
|
|
2918
|
-
if (ts.isPropertyAccessExpression(expression) || ts.isPropertyAccessChain(expression))
|
|
2919
|
-
return `${callName(expression.expression) ?? expression.expression.getText()}.${expression.name.text}`;
|
|
2920
|
-
return undefined;
|
|
2921
|
-
}
|
|
2922
|
-
function pendingIs(op) {
|
|
2923
|
-
return pendingIsAt(0, op);
|
|
2924
|
-
}
|
|
2925
|
-
function pendingIsAt(index, op) {
|
|
2926
|
-
return { kind: "eq", args: [{ kind: "read", var: "sys:pending", path: [String(index), "opId"] }, { kind: "lit", value: op }] };
|
|
2927
|
-
}
|
|
2928
|
-
function componentNameFor(node) {
|
|
2929
|
-
if (ts.isFunctionDeclaration(node) && node.name && startsUppercase(node.name.text))
|
|
2930
|
-
return node.name.text;
|
|
2931
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && startsUppercase(node.name.text))
|
|
2932
|
-
return node.name.text;
|
|
2933
|
-
return undefined;
|
|
2934
|
-
}
|
|
2935
|
-
function providerComponentNames(source) {
|
|
2936
|
-
const names = new Set();
|
|
2937
|
-
const visit = (node) => {
|
|
2938
|
-
const name = componentNameFor(node);
|
|
2939
|
-
if (name && node.getText(source).includes(".Provider"))
|
|
2940
|
-
names.add(name);
|
|
2941
|
-
ts.forEachChild(node, visit);
|
|
2942
|
-
};
|
|
2943
|
-
visit(source);
|
|
2944
|
-
return names;
|
|
2945
|
-
}
|
|
2946
|
-
function startsUppercase(value) {
|
|
2947
|
-
return /^[A-Z]/.test(value);
|
|
2948
|
-
}
|
|
2949
|
-
function firstValue(domain) {
|
|
2950
|
-
switch (domain.kind) {
|
|
2951
|
-
case "bool":
|
|
2952
|
-
return false;
|
|
2953
|
-
case "enum":
|
|
2954
|
-
return domain.values[0] ?? "";
|
|
2955
|
-
case "boundedInt":
|
|
2956
|
-
return domain.min;
|
|
2957
|
-
case "option":
|
|
2958
|
-
return null;
|
|
2959
|
-
case "record":
|
|
2960
|
-
return Object.fromEntries(Object.entries(domain.fields).map(([key, field]) => [key, firstValue(field)]));
|
|
2961
|
-
case "tagged": {
|
|
2962
|
-
const [tagValue, variant] = Object.entries(domain.variants)[0] ?? ["unknown", { kind: "record", fields: {} }];
|
|
2963
|
-
return { ...firstValue(variant), [domain.tag]: tagValue };
|
|
2964
|
-
}
|
|
2965
|
-
case "tokens":
|
|
2966
|
-
return domain.names?.[0] ?? "tok1";
|
|
2967
|
-
case "lengthCat":
|
|
2968
|
-
return "0";
|
|
2969
|
-
case "boundedList":
|
|
2970
|
-
return [];
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
function lineAndColumn(source, node) {
|
|
2974
|
-
const pos = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
2975
|
-
return { line: pos.line + 1, column: pos.character + 1 };
|
|
2976
|
-
}
|
|
2977
|
-
//# sourceMappingURL=index.js.map
|