octane 0.2.11 → 0.2.13
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/README.md +5 -0
- package/dist/behavior-root.d.ts +13 -3
- package/dist/behavior-root.js +28 -5
- package/dist/cjs/behavior-root.cjs +32 -6
- package/dist/cjs/class-names.cjs +49 -0
- package/dist/cjs/constants.cjs +9 -0
- package/dist/cjs/context-epoch.cjs +38 -0
- package/dist/cjs/css.cjs +6 -27
- package/dist/cjs/data-encoding.cjs +160 -0
- package/dist/cjs/dom-binding-handoff.cjs +93 -0
- package/dist/cjs/dom-binding-protocol.cjs +115 -0
- package/dist/cjs/dom-binding-styles.cjs +292 -0
- package/dist/cjs/dom-bindings.cjs +700 -0
- package/dist/cjs/dom-focused-move.cjs +66 -0
- package/dist/cjs/dom-stage.cjs +896 -0
- package/dist/cjs/dom-tables.cjs +4 -94
- package/dist/cjs/error-codes.client.generated.cjs +33 -0
- package/dist/cjs/error-codes.server.generated.cjs +24 -0
- package/dist/cjs/hydration/control-capture.cjs +244 -0
- package/dist/cjs/hydration/event-capture.cjs +125 -43
- package/dist/cjs/hydration/interaction-config.cjs +50 -2
- package/dist/cjs/hydration-markers.cjs +37 -0
- package/dist/cjs/independent-hydration-protocol.cjs +106 -0
- package/dist/cjs/index.cjs +9 -0
- package/dist/cjs/runtime.cjs +6397 -1683
- package/dist/cjs/runtime.server.cjs +1465 -213
- package/dist/cjs/server/early-signals.cjs +45 -0
- package/dist/cjs/server/index.cjs +47 -0
- package/dist/cjs/server/rpc-batch.cjs +149 -0
- package/dist/cjs/server/rpc-stream.cjs +146 -0
- package/dist/cjs/server/rpc.cjs +34 -15
- package/dist/cjs/server/signal-query-observation.cjs +158 -0
- package/dist/cjs/server/streamed-region.cjs +52 -0
- package/dist/cjs/server/streamed-signals.cjs +512 -0
- package/dist/cjs/server-call.cjs +103 -0
- package/dist/cjs/server-rpc-batch-client.cjs +141 -0
- package/dist/cjs/server-rpc-client.cjs +94 -27
- package/dist/cjs/server-rpc-protocol.cjs +141 -0
- package/dist/cjs/server-rpc-stream-client.cjs +118 -0
- package/dist/cjs/signals/actions.cjs +474 -0
- package/dist/cjs/signals/client.cjs +4 -1
- package/dist/cjs/signals/computations.cjs +460 -0
- package/dist/cjs/signals/control-binding.cjs +368 -0
- package/dist/cjs/signals/derived-facade.cjs +50 -0
- package/dist/cjs/signals/document-owner.cjs +71 -0
- package/dist/cjs/signals/early-values.cjs +155 -0
- package/dist/cjs/signals/encoding.cjs +6 -95
- package/dist/cjs/signals/engine.cjs +276 -12
- package/dist/cjs/signals/errors.cjs +18 -0
- package/dist/cjs/signals/facade.cjs +477 -0
- package/dist/cjs/signals/graph.cjs +166 -20
- package/dist/cjs/signals/handle-protocol.cjs +36 -0
- package/dist/cjs/signals/index.cjs +81 -2
- package/dist/cjs/signals/native-read-client.cjs +10 -0
- package/dist/cjs/signals/owner-context.cjs +112 -0
- package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
- package/dist/cjs/signals/query-facade.cjs +72 -0
- package/dist/cjs/signals/read-protocol.cjs +54 -0
- package/dist/cjs/signals/requests.cjs +287 -14
- package/dist/cjs/signals/scalar-computations.cjs +76 -0
- package/dist/cjs/signals/server.cjs +4 -1
- package/dist/cjs/signals/transition-candidate.cjs +567 -0
- package/dist/cjs/signals/transition-state.cjs +106 -0
- package/dist/cjs/signals/types.cjs +50 -0
- package/dist/cjs/stream-protocol.cjs +3 -0
- package/dist/cjs/streamed-signals-protocol.cjs +159 -0
- package/dist/cjs/style-values.cjs +122 -0
- package/dist/cjs/trusted-html.cjs +30 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/class-names.d.ts +5 -0
- package/dist/class-names.js +25 -0
- package/dist/compiler/bundler.js +51 -22
- package/dist/compiler/compile.js +2534 -581
- package/dist/compiler/dom-binding-program.js +935 -0
- package/dist/compiler/dom-binding-request.js +57 -0
- package/dist/compiler/dom-bindings.js +2245 -0
- package/dist/compiler/hook-deps.js +759 -10
- package/dist/compiler/hydrate-boundaries.js +358 -15
- package/dist/compiler/index.d.ts +39 -2
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/native-attribute-reads.js +127 -0
- package/dist/compiler/native-change-diagnostics.js +32 -38
- package/dist/compiler/native-read-diagnostics.js +69 -8
- package/dist/compiler/native-read-types.js +80 -34
- package/dist/compiler/parser.node.js +1 -1
- package/dist/compiler/renderer-boundaries.js +40 -0
- package/dist/compiler/server-context.js +118 -0
- package/dist/compiler/signal-attempt-reads.js +479 -0
- package/dist/compiler/signal-declarations.js +557 -0
- package/dist/compiler/signal-start-reads.js +413 -0
- package/dist/compiler/slot-hooks.d.ts +3 -1
- package/dist/compiler/slot-hooks.js +168 -34
- package/dist/compiler/strong-auto-memo.js +90 -0
- package/dist/compiler/strong-html.js +143 -0
- package/dist/compiler/strong-mode.js +764 -66
- package/dist/compiler/strong-template-policy.js +307 -0
- package/dist/compiler/template-ir.js +200 -0
- package/dist/compiler/typescript.d.ts +2 -0
- package/dist/compiler/typescript.js +2 -0
- package/dist/compiler/vite.d.ts +3 -1
- package/dist/compiler/vite.js +122 -4
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.d.ts +6 -1
- package/dist/compiler/volar.js +750 -1433
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +10 -0
- package/dist/context-epoch.d.ts +2 -0
- package/dist/context-epoch.js +13 -0
- package/dist/css.d.ts +2 -5
- package/dist/css.js +2 -23
- package/dist/data-encoding.d.ts +9 -0
- package/dist/data-encoding.js +134 -0
- package/dist/dom-binding-classes.d.ts +15 -0
- package/dist/dom-binding-classes.js +129 -0
- package/dist/dom-binding-controls.d.ts +2 -0
- package/dist/dom-binding-controls.js +6 -0
- package/dist/dom-binding-handoff.d.ts +55 -0
- package/dist/dom-binding-handoff.js +62 -0
- package/dist/dom-binding-program.d.ts +300 -0
- package/dist/dom-binding-program.js +1625 -0
- package/dist/dom-binding-projections.d.ts +15 -0
- package/dist/dom-binding-projections.js +187 -0
- package/dist/dom-binding-protocol.d.ts +13 -0
- package/dist/dom-binding-protocol.js +88 -0
- package/dist/dom-binding-signals.d.ts +21 -0
- package/dist/dom-binding-signals.js +109 -0
- package/dist/dom-binding-styles.d.ts +17 -0
- package/dist/dom-binding-styles.js +272 -0
- package/dist/dom-bindings.d.ts +89 -0
- package/dist/dom-bindings.js +670 -0
- package/dist/dom-focused-move.d.ts +6 -0
- package/dist/dom-focused-move.js +42 -0
- package/dist/dom-stage.d.ts +52 -0
- package/dist/dom-stage.js +872 -0
- package/dist/dom-tables.d.ts +1 -3
- package/dist/dom-tables.js +1 -91
- package/dist/error-codes.client.generated.d.ts +9 -0
- package/dist/error-codes.client.generated.js +33 -0
- package/dist/error-codes.server.generated.d.ts +6 -0
- package/dist/error-codes.server.generated.js +24 -0
- package/dist/hydration/control-capture.d.ts +62 -0
- package/dist/hydration/control-capture.js +223 -0
- package/dist/hydration/document-lifecycle.d.ts +23 -0
- package/dist/hydration/document-lifecycle.js +80 -0
- package/dist/hydration/event-capture.d.ts +20 -1
- package/dist/hydration/event-capture.js +128 -42
- package/dist/hydration/independent-island.d.ts +45 -0
- package/dist/hydration/independent-island.js +233 -0
- package/dist/hydration/index.d.ts +8 -1
- package/dist/hydration/index.js +55 -1
- package/dist/hydration/interaction-config.d.ts +5 -0
- package/dist/hydration/interaction-config.js +44 -1
- package/dist/hydration/stream-delivery.d.ts +32 -0
- package/dist/hydration/stream-delivery.js +333 -0
- package/dist/hydration/stream-receiver.d.ts +26 -0
- package/dist/hydration/stream-receiver.js +204 -0
- package/dist/hydration/stream-result-receiver.d.ts +70 -0
- package/dist/hydration/stream-result-receiver.js +246 -0
- package/dist/hydration/streamed-signals.d.ts +44 -0
- package/dist/hydration/streamed-signals.js +186 -0
- package/dist/hydration/types.d.ts +6 -0
- package/dist/hydration-markers.d.ts +7 -0
- package/dist/hydration-markers.js +10 -0
- package/dist/independent-hydration-protocol.d.ts +31 -0
- package/dist/independent-hydration-protocol.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/internal/signal-read.d.ts +1 -0
- package/dist/internal/signal-read.js +4 -0
- package/dist/jsx-runtime-strong.d.ts +33 -0
- package/dist/jsx-runtime.d.ts +58 -2
- package/dist/public-types.d.ts +15 -1
- package/dist/runtime.d.ts +297 -23
- package/dist/runtime.js +6405 -1679
- package/dist/runtime.server.d.ts +110 -27
- package/dist/runtime.server.js +1458 -213
- package/dist/server/early-signals.d.ts +14 -0
- package/dist/server/early-signals.js +34 -0
- package/dist/server/index.d.ts +11 -2
- package/dist/server/index.js +50 -1
- package/dist/server/rpc-batch.d.ts +17 -0
- package/dist/server/rpc-batch.js +129 -0
- package/dist/server/rpc-stream.d.ts +9 -0
- package/dist/server/rpc-stream.js +125 -0
- package/dist/server/rpc.d.ts +5 -1
- package/dist/server/rpc.js +37 -13
- package/dist/server/signal-query-observation.d.ts +3 -0
- package/dist/server/signal-query-observation.js +134 -0
- package/dist/server/streamed-region.d.ts +15 -0
- package/dist/server/streamed-region.js +30 -0
- package/dist/server/streamed-signals.d.ts +45 -0
- package/dist/server/streamed-signals.js +488 -0
- package/dist/server-call.d.ts +41 -0
- package/dist/server-call.js +75 -0
- package/dist/server-rpc-batch-client.d.ts +7 -0
- package/dist/server-rpc-batch-client.js +119 -0
- package/dist/server-rpc-client.d.ts +3 -1
- package/dist/server-rpc-client.js +99 -27
- package/dist/server-rpc-protocol.d.ts +47 -0
- package/dist/server-rpc-protocol.js +109 -0
- package/dist/server-rpc-stream-client.d.ts +5 -0
- package/dist/server-rpc-stream-client.js +98 -0
- package/dist/signals/actions.d.ts +12 -0
- package/dist/signals/actions.js +466 -0
- package/dist/signals/client.d.ts +2 -0
- package/dist/signals/client.js +1 -0
- package/dist/signals/computations.d.ts +37 -0
- package/dist/signals/computations.js +455 -0
- package/dist/signals/control-binding.d.ts +31 -0
- package/dist/signals/control-binding.js +353 -0
- package/dist/signals/derived-facade.d.ts +3 -0
- package/dist/signals/derived-facade.js +25 -0
- package/dist/signals/document-owner.d.ts +10 -0
- package/dist/signals/document-owner.js +43 -0
- package/dist/signals/early-values.d.ts +24 -0
- package/dist/signals/early-values.js +124 -0
- package/dist/signals/encoding.d.ts +1 -3
- package/dist/signals/encoding.js +3 -93
- package/dist/signals/engine.d.ts +53 -4
- package/dist/signals/engine.js +275 -16
- package/dist/signals/errors.d.ts +7 -0
- package/dist/signals/errors.js +16 -0
- package/dist/signals/facade.d.ts +74 -0
- package/dist/signals/facade.js +452 -0
- package/dist/signals/graph.d.ts +52 -6
- package/dist/signals/graph.js +174 -19
- package/dist/signals/handle-protocol.d.ts +4 -0
- package/dist/signals/handle-protocol.js +11 -0
- package/dist/signals/index.d.ts +12 -3
- package/dist/signals/index.js +71 -2
- package/dist/signals/native-read-client.d.ts +3 -0
- package/dist/signals/native-read-client.js +10 -0
- package/dist/signals/owner-context.d.ts +22 -0
- package/dist/signals/owner-context.js +77 -0
- package/dist/signals/query-attempt-observer.d.ts +49 -0
- package/dist/signals/query-attempt-observer.js +41 -0
- package/dist/signals/query-facade.d.ts +3 -0
- package/dist/signals/query-facade.js +49 -0
- package/dist/signals/read-protocol.d.ts +37 -2
- package/dist/signals/read-protocol.js +44 -0
- package/dist/signals/requests.d.ts +38 -5
- package/dist/signals/requests.js +298 -15
- package/dist/signals/scalar-computations.d.ts +2 -0
- package/dist/signals/scalar-computations.js +60 -0
- package/dist/signals/server.d.ts +2 -0
- package/dist/signals/server.js +1 -0
- package/dist/signals/transition-candidate.d.ts +71 -0
- package/dist/signals/transition-candidate.js +557 -0
- package/dist/signals/transition-state.d.ts +51 -0
- package/dist/signals/transition-state.js +70 -0
- package/dist/signals/types.d.ts +95 -5
- package/dist/signals/types.js +20 -0
- package/dist/stream-protocol.d.ts +0 -5
- package/dist/stream-protocol.js +3 -0
- package/dist/streamed-signals-protocol.d.ts +60 -0
- package/dist/streamed-signals-protocol.js +130 -0
- package/dist/style-values.d.ts +3 -0
- package/dist/style-values.js +96 -0
- package/dist/trusted-html.d.ts +17 -0
- package/dist/trusted-html.js +6 -0
- package/dist/universal-core.d.ts +6 -0
- package/dist/universal-core.js +214 -202
- package/dist/version.js +1 -1
- package/package.json +48 -5
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
analyzeStrongHookBindings,
|
|
3
|
+
analyzeStrongHookPolicies,
|
|
4
|
+
collectReassignedBindings,
|
|
5
|
+
} from './hook-deps.js';
|
|
6
|
+
import { createRendererRegionResolver } from './renderer-boundaries.js';
|
|
7
|
+
import { analyzeStrongHTML } from './strong-html.js';
|
|
8
|
+
import { analyzeNativeChangeDiagnostics } from './native-change-diagnostics.js';
|
|
9
|
+
import { createStrongTemplatePolicy } from './strong-template-policy.js';
|
|
2
10
|
|
|
3
11
|
const STATE_HOOKS = new Set(['useState', 'useReducer', 'useLinkedState']);
|
|
4
12
|
const EFFECT_HOOKS = new Set(['useEffect', 'useLayoutEffect', 'useInsertionEffect']);
|
|
13
|
+
const EFFECT_CONTROL_TYPES = new Set([
|
|
14
|
+
'ForStatement',
|
|
15
|
+
'ForInStatement',
|
|
16
|
+
'ForOfStatement',
|
|
17
|
+
'WhileStatement',
|
|
18
|
+
'DoWhileStatement',
|
|
19
|
+
'SwitchStatement',
|
|
20
|
+
'LabeledStatement',
|
|
21
|
+
]);
|
|
5
22
|
const ARRAY_MUTATORS = new Set([
|
|
6
23
|
'copyWithin',
|
|
7
24
|
'fill',
|
|
@@ -50,20 +67,12 @@ const TRUTHY_VALUE = 8;
|
|
|
50
67
|
const UNKNOWN_VALUE = NULLISH_VALUE | FALSY_VALUE | TRUTHY_VALUE;
|
|
51
68
|
const UNKNOWN_PRIMITIVE = Symbol('unknown primitive');
|
|
52
69
|
const NO_RETURN_VALUE = Symbol('no return value');
|
|
70
|
+
const NORMAL_COMPLETIONS = new Set(['normal']);
|
|
53
71
|
const OTHER_BINDING = { kind: 'other' };
|
|
54
72
|
const SNAPSHOT_BINDING = { kind: 'snapshot' };
|
|
55
|
-
const ARRAY_SNAPSHOT_BINDING = { kind: 'snapshot', array: true };
|
|
56
73
|
const STATE_GETTER_BINDING = { kind: 'getter' };
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
snapshot: SNAPSHOT_BINDING,
|
|
60
|
-
getter: STATE_GETTER_BINDING,
|
|
61
|
-
};
|
|
62
|
-
const ARRAY_STATE_TUPLE_BINDING = {
|
|
63
|
-
kind: 'state-tuple',
|
|
64
|
-
snapshot: ARRAY_SNAPSHOT_BINDING,
|
|
65
|
-
getter: STATE_GETTER_BINDING,
|
|
66
|
-
};
|
|
74
|
+
const PROP_BINDING = { kind: 'prop' };
|
|
75
|
+
const FETCH_BINDING = { kind: 'fetch' };
|
|
67
76
|
const UNDEFINED_BINDING = { kind: 'constant', value: UNDEFINED_VALUE, primitive: undefined };
|
|
68
77
|
const GLOBAL_OBJECT_BINDING = { kind: 'global-object' };
|
|
69
78
|
const AMBIENT_GLOBAL_BINDINGS = new Map(
|
|
@@ -172,6 +181,9 @@ export const STRONG_RETAINED_ROW_MUTATION = 'OCTANE_STRONG_RETAINED_ROW_MUTATION
|
|
|
172
181
|
export const STRONG_RENDER_IMPURE_CALL = 'OCTANE_STRONG_RENDER_IMPURE_CALL';
|
|
173
182
|
export const STRONG_RENDER_EFFECT_EVENT_CALL = 'OCTANE_STRONG_RENDER_EFFECT_EVENT_CALL';
|
|
174
183
|
export const STRONG_EFFECT_EVENT_DEPENDENCY = 'OCTANE_STRONG_EFFECT_EVENT_DEPENDENCY';
|
|
184
|
+
export const STRONG_EFFECT_DATA_FETCH = 'OCTANE_STRONG_EFFECT_DATA_FETCH';
|
|
185
|
+
export const STRONG_EFFECT_CHAIN = 'OCTANE_STRONG_EFFECT_CHAIN';
|
|
186
|
+
export const STRONG_UNLINKED_PROP_STATE = 'OCTANE_STRONG_UNLINKED_PROP_STATE';
|
|
175
187
|
export const STRONG_DIRECTIVE_PLACEMENT = 'OCTANE_STRONG_DIRECTIVE_PLACEMENT';
|
|
176
188
|
export const STRONG_HOOK_LOCALITY = 'OCTANE_STRONG_HOOK_LOCALITY';
|
|
177
189
|
export const STRONG_EVENT_HANDLER_LOCALITY = 'OCTANE_STRONG_EVENT_HANDLER_LOCALITY';
|
|
@@ -240,12 +252,20 @@ function bindSnapshotPattern(pattern, value, bind) {
|
|
|
240
252
|
} else if (pattern?.type === 'ObjectPattern') {
|
|
241
253
|
for (const property of pattern.properties ?? []) {
|
|
242
254
|
if (property.type === 'Property') {
|
|
243
|
-
bindSnapshotPattern(
|
|
255
|
+
bindSnapshotPattern(
|
|
256
|
+
property.value,
|
|
257
|
+
value.state ? { kind: 'snapshot', state: value.state } : SNAPSHOT_BINDING,
|
|
258
|
+
bind,
|
|
259
|
+
);
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
} else if (pattern?.type === 'ArrayPattern') {
|
|
247
263
|
for (const element of pattern.elements ?? []) {
|
|
248
|
-
bindSnapshotPattern(
|
|
264
|
+
bindSnapshotPattern(
|
|
265
|
+
element,
|
|
266
|
+
value.state ? { kind: 'snapshot', state: value.state } : SNAPSHOT_BINDING,
|
|
267
|
+
bind,
|
|
268
|
+
);
|
|
249
269
|
}
|
|
250
270
|
}
|
|
251
271
|
// Rest copies and default expressions can produce new mutable values.
|
|
@@ -306,7 +326,9 @@ function predeclareStatements(statements, scope) {
|
|
|
306
326
|
for (const original of statements ?? []) {
|
|
307
327
|
const node = declarationOf(original);
|
|
308
328
|
if (node?.type === 'ImportDeclaration') {
|
|
309
|
-
const octane =
|
|
329
|
+
const octane =
|
|
330
|
+
(node.source?.value === 'octane' || node.source?.value === 'octane/server') &&
|
|
331
|
+
node.importKind !== 'type';
|
|
310
332
|
for (const specifier of node.specifiers ?? []) {
|
|
311
333
|
if (!specifier.local?.name || specifier.importKind === 'type') continue;
|
|
312
334
|
if (octane && specifier.type === 'ImportNamespaceSpecifier') {
|
|
@@ -343,6 +365,7 @@ function createScope(
|
|
|
343
365
|
kind,
|
|
344
366
|
bindings: new BindingMap(parent?.bindings.clock ?? { value: 0 }),
|
|
345
367
|
isReassigned,
|
|
368
|
+
hookNames: parent?.hookNames,
|
|
346
369
|
};
|
|
347
370
|
for (const param of params) addPatternNames(param, scope.bindings, { kind: 'other' });
|
|
348
371
|
predeclareStatements(statements, scope);
|
|
@@ -375,12 +398,15 @@ function resolveScope(scope, name) {
|
|
|
375
398
|
}
|
|
376
399
|
|
|
377
400
|
function importedHook(callee, scope) {
|
|
401
|
+
if (scope.hookNames !== undefined) return scope.hookNames.get(callee) ?? null;
|
|
378
402
|
const value = unwrap(callee);
|
|
379
403
|
if (value?.type === 'Identifier') {
|
|
380
404
|
const binding = resolve(scope, value.name);
|
|
381
405
|
return binding?.kind === 'hook' ? binding.hook : null;
|
|
382
406
|
}
|
|
383
|
-
|
|
407
|
+
// An imported namespace is never nullish. Optional access to its known
|
|
408
|
+
// hooks has the same execution phase as ordinary namespace access.
|
|
409
|
+
if (value?.type !== 'MemberExpression') return null;
|
|
384
410
|
const object = unwrap(value.object);
|
|
385
411
|
if (object?.type !== 'Identifier' || resolve(scope, object.name)?.kind !== 'namespace') {
|
|
386
412
|
return null;
|
|
@@ -456,7 +482,7 @@ function strongDirectives(ast) {
|
|
|
456
482
|
// can be declared there; a value used by siblings belongs to their parent.
|
|
457
483
|
// Keep this source-level check separate from the phase analysis above: it never
|
|
458
484
|
// annotates the parser tree or changes the code emitted for a valid module.
|
|
459
|
-
function strongLocalityDiagnostics(ast, source, filename, isReassigned) {
|
|
485
|
+
function strongLocalityDiagnostics(ast, source, filename, isReassigned, hookNames) {
|
|
460
486
|
const diagnostics = [];
|
|
461
487
|
const values = [];
|
|
462
488
|
const effects = [];
|
|
@@ -466,6 +492,7 @@ function strongLocalityDiagnostics(ast, source, filename, isReassigned) {
|
|
|
466
492
|
const hostAttributes = new WeakSet();
|
|
467
493
|
const componentHandlers = new WeakSet();
|
|
468
494
|
const moduleScope = createScope(null, 'module', ast.body ?? []);
|
|
495
|
+
moduleScope.hookNames = hookNames;
|
|
469
496
|
const mayHaveHoistedVars = source.includes('var');
|
|
470
497
|
let activeHandler = null;
|
|
471
498
|
let activeDerivedValue = null;
|
|
@@ -1286,6 +1313,25 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
1286
1313
|
}
|
|
1287
1314
|
const enabled = options.strong === true || directives.enabled;
|
|
1288
1315
|
if (!enabled) return { enabled: false, diagnostics };
|
|
1316
|
+
const strongHookAnalysis = analyzeStrongHookBindings(ast, options);
|
|
1317
|
+
const hookNames = new WeakMap();
|
|
1318
|
+
for (const [call, name] of strongHookAnalysis.callNames) {
|
|
1319
|
+
if (
|
|
1320
|
+
LOCAL_VALUE_HOOKS.has(name) ||
|
|
1321
|
+
EFFECT_HOOKS.has(name) ||
|
|
1322
|
+
name === 'useImperativeHandle' ||
|
|
1323
|
+
name === 'memo' ||
|
|
1324
|
+
name === 'lazy'
|
|
1325
|
+
)
|
|
1326
|
+
hookNames.set(call.callee, name);
|
|
1327
|
+
}
|
|
1328
|
+
options = {
|
|
1329
|
+
...options,
|
|
1330
|
+
strongHookAnalysis,
|
|
1331
|
+
rendererRegionResolver:
|
|
1332
|
+
options.rendererRegionResolver ??
|
|
1333
|
+
createRendererRegionResolver(ast, source, filename, options),
|
|
1334
|
+
};
|
|
1289
1335
|
|
|
1290
1336
|
let reassignedBindings;
|
|
1291
1337
|
function isReassigned(identifier) {
|
|
@@ -1293,6 +1339,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
1293
1339
|
return reassignedBindings.has(identifier);
|
|
1294
1340
|
}
|
|
1295
1341
|
const moduleScope = createScope(null, 'module', ast.body ?? [], [], isReassigned);
|
|
1342
|
+
moduleScope.hookNames = hookNames;
|
|
1296
1343
|
const mutableModuleDeclarations = new Map();
|
|
1297
1344
|
const reassignedModuleNames = new Map();
|
|
1298
1345
|
const moduleAmbientAliases = new Map();
|
|
@@ -1330,6 +1377,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
1330
1377
|
const activeCallbacks = new Set();
|
|
1331
1378
|
const activeReturnCallbacks = new Set();
|
|
1332
1379
|
const callResults = new WeakMap();
|
|
1380
|
+
const stateTuples = new WeakMap();
|
|
1381
|
+
const effectRecords = [];
|
|
1382
|
+
const effectBranchExits = new WeakMap();
|
|
1383
|
+
const unlinkedPropInitializers = new Set();
|
|
1384
|
+
let renderOwner = null;
|
|
1385
|
+
let currentEffect = null;
|
|
1386
|
+
let currentEffectControl = null;
|
|
1387
|
+
let currentFunction = null;
|
|
1388
|
+
let fetchContinuation = false;
|
|
1389
|
+
let collectEffectReads = true;
|
|
1390
|
+
let effectOwnsWrites = true;
|
|
1333
1391
|
const reportedDiagnostics = new WeakMap();
|
|
1334
1392
|
const hoistedVarNames = new WeakMap();
|
|
1335
1393
|
const mayHaveHoistedVars = source.includes('var');
|
|
@@ -1367,7 +1425,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
1367
1425
|
renderOutputs.set(fn, result);
|
|
1368
1426
|
return result;
|
|
1369
1427
|
}
|
|
1428
|
+
// Hook-named module functions keep unknown arguments even when they build
|
|
1429
|
+
// JSX: their callers, not props, supply the values.
|
|
1430
|
+
const hookNamedRoots = new WeakSet();
|
|
1370
1431
|
function addNamedRenderRoot(fn, name) {
|
|
1432
|
+
if (FUNCTION_TYPES.has(fn?.type) && /^(?:unstable_)?use[A-Z0-9]/.test(name)) {
|
|
1433
|
+
hookNamedRoots.add(fn);
|
|
1434
|
+
}
|
|
1371
1435
|
if (
|
|
1372
1436
|
FUNCTION_TYPES.has(fn?.type) &&
|
|
1373
1437
|
(/^(?:unstable_)?use[A-Z0-9]/.test(name) || (/^[A-Z]/.test(name) && hasRenderOutput(fn)))
|
|
@@ -1411,6 +1475,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
1411
1475
|
let currentFunctionChecksImpureCalls = false;
|
|
1412
1476
|
let currentFunctionChecksRenderReads = false;
|
|
1413
1477
|
let currentRetainedRowScope = null;
|
|
1478
|
+
const templatePolicy = createStrongTemplatePolicy({
|
|
1479
|
+
ast,
|
|
1480
|
+
report,
|
|
1481
|
+
resolve,
|
|
1482
|
+
unwrap,
|
|
1483
|
+
callableValue,
|
|
1484
|
+
staticPrimitiveValue,
|
|
1485
|
+
source,
|
|
1486
|
+
filename,
|
|
1487
|
+
options,
|
|
1488
|
+
});
|
|
1414
1489
|
|
|
1415
1490
|
function predeclareHoistedVars(node, scope) {
|
|
1416
1491
|
if (!mayHaveHoistedVars || node == null) return;
|
|
@@ -2071,7 +2146,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2071
2146
|
statement.type === 'ReturnStatement' ||
|
|
2072
2147
|
statement.type === 'ThrowStatement' ||
|
|
2073
2148
|
statement.type === 'BreakStatement' ||
|
|
2074
|
-
statement.type === 'ContinueStatement'
|
|
2149
|
+
statement.type === 'ContinueStatement' ||
|
|
2150
|
+
(currentEffect !== null && branchAlwaysExits(statement))
|
|
2075
2151
|
) {
|
|
2076
2152
|
break;
|
|
2077
2153
|
}
|
|
@@ -2145,16 +2221,18 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2145
2221
|
? expressionBinding(parameter.right, parameterScope)
|
|
2146
2222
|
: null;
|
|
2147
2223
|
value =
|
|
2148
|
-
value
|
|
2149
|
-
?
|
|
2150
|
-
:
|
|
2151
|
-
?
|
|
2152
|
-
:
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2224
|
+
value?.kind === 'prop'
|
|
2225
|
+
? value
|
|
2226
|
+
: value.kind === 'constant' && value.primitive === undefined
|
|
2227
|
+
? defaultValue
|
|
2228
|
+
: definitelyDefined(value)
|
|
2229
|
+
? value
|
|
2230
|
+
: defaultValue?.kind === 'getter' ||
|
|
2231
|
+
defaultValue?.kind === 'state-tuple' ||
|
|
2232
|
+
defaultValue === GLOBAL_OBJECT_BINDING ||
|
|
2233
|
+
defaultValue?.kind === 'ambient'
|
|
2234
|
+
? defaultValue
|
|
2235
|
+
: OTHER_BINDING;
|
|
2158
2236
|
visitPatternExpressions(
|
|
2159
2237
|
parameter.left,
|
|
2160
2238
|
parameterScope,
|
|
@@ -2166,11 +2244,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2166
2244
|
);
|
|
2167
2245
|
bindStateGetterPattern(parameter.left, value, parameterScope, bindGetter);
|
|
2168
2246
|
bindAmbientPattern(parameter.left, value, parameterScope, bindGetter);
|
|
2247
|
+
if (value?.kind === 'prop') bindPropPattern(parameter.left, bindGetter);
|
|
2169
2248
|
parameter = parameter.left;
|
|
2170
2249
|
} else {
|
|
2171
2250
|
visitPatternExpressions(parameter, parameterScope, phase, value?.kind === 'ref', value);
|
|
2172
2251
|
bindStateGetterPattern(parameter, value, parameterScope, bindGetter);
|
|
2173
2252
|
bindAmbientPattern(parameter, value, parameterScope, bindGetter);
|
|
2253
|
+
if (value?.kind === 'prop') bindPropPattern(parameter, bindGetter);
|
|
2174
2254
|
}
|
|
2175
2255
|
if (parameter.type === 'Identifier' && !isReassigned(parameter)) {
|
|
2176
2256
|
parameterScope.bindings.set(parameter.name, value);
|
|
@@ -2180,6 +2260,20 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2180
2260
|
}
|
|
2181
2261
|
|
|
2182
2262
|
function visitFunction(node, parentScope, phase, args = null) {
|
|
2263
|
+
const enclosingFunction = currentFunction;
|
|
2264
|
+
const enclosingOwner = renderOwner;
|
|
2265
|
+
const enclosingFetchContinuation = fetchContinuation;
|
|
2266
|
+
currentFunction = node;
|
|
2267
|
+
if (
|
|
2268
|
+
(phase === 'render' && renderOwner === null) ||
|
|
2269
|
+
(!activeCallbacks.has(node) && hasRenderOutput(node))
|
|
2270
|
+
)
|
|
2271
|
+
renderOwner = node;
|
|
2272
|
+
if (args === null && phase === 'render' && hasRenderOutput(node) && !hookNamedRoots.has(node)) {
|
|
2273
|
+
// Component arguments are immutable prop projections. Custom hook
|
|
2274
|
+
// arguments remain unknown unless a caller supplies a proven value.
|
|
2275
|
+
args = (node.params ?? []).map(() => PROP_BINDING);
|
|
2276
|
+
}
|
|
2183
2277
|
const enclosingFunctionIsAsync = currentFunctionIsAsync;
|
|
2184
2278
|
const enclosingFunctionChecksImpureCalls = currentFunctionChecksImpureCalls;
|
|
2185
2279
|
const enclosingFunctionChecksRenderReads = currentFunctionChecksRenderReads;
|
|
@@ -2214,9 +2308,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2214
2308
|
const executionPhase = visitStatements(body.body, functionScope, phase);
|
|
2215
2309
|
if (body.render) visit(body.render, functionScope, executionPhase);
|
|
2216
2310
|
} else {
|
|
2311
|
+
// An expression body returns its value, so it can supply the cleanup
|
|
2312
|
+
// exactly like a block body's return statement.
|
|
2313
|
+
if (currentEffect !== null && effectCallbackIsCurrent(currentEffect.callback)) {
|
|
2314
|
+
markEffectCleanup(body, functionScope);
|
|
2315
|
+
}
|
|
2217
2316
|
visit(body, functionScope, phase);
|
|
2218
2317
|
}
|
|
2219
2318
|
} finally {
|
|
2319
|
+
currentFunction = enclosingFunction;
|
|
2320
|
+
renderOwner = enclosingOwner;
|
|
2321
|
+
fetchContinuation = enclosingFetchContinuation;
|
|
2220
2322
|
currentFunctionIsAsync = enclosingFunctionIsAsync;
|
|
2221
2323
|
currentFunctionChecksImpureCalls = enclosingFunctionChecksImpureCalls;
|
|
2222
2324
|
currentFunctionChecksRenderReads = enclosingFunctionChecksRenderReads;
|
|
@@ -2331,6 +2433,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2331
2433
|
bindAmbientPattern(declaration.id, ambient, scope, bind);
|
|
2332
2434
|
return;
|
|
2333
2435
|
}
|
|
2436
|
+
if (declarationKind === 'const' || !isReassigned(declaration.id)) {
|
|
2437
|
+
if (fetchFunction(initial, scope)) {
|
|
2438
|
+
bind(declaration.id, FETCH_BINDING);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
const request = currentEffect === null ? null : fetchRequest(initial, scope);
|
|
2442
|
+
if (request !== null) {
|
|
2443
|
+
bind(declaration.id, { kind: 'fetch-request', request });
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2334
2447
|
const stateTuple = stateTupleBinding(initial, scope);
|
|
2335
2448
|
const snapshot =
|
|
2336
2449
|
stateTuple === null && declarationKind === 'const' ? snapshotBinding(initial, scope) : null;
|
|
@@ -2338,7 +2451,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2338
2451
|
bindSnapshotPattern(declaration.id.elements?.[0], stateTuple.snapshot, bind);
|
|
2339
2452
|
const element = declaration.id.elements?.[1];
|
|
2340
2453
|
const setter = element?.type === 'AssignmentPattern' ? element.left : element;
|
|
2341
|
-
bind(setter,
|
|
2454
|
+
bind(setter, stateTuple.setter);
|
|
2342
2455
|
bindStateGetterPattern(declaration.id, stateTuple, scope, bind);
|
|
2343
2456
|
} else if (declaration.id?.type === 'ObjectPattern' && stateTuple) {
|
|
2344
2457
|
for (const property of declaration.id.properties ?? []) {
|
|
@@ -2351,7 +2464,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2351
2464
|
const value = property.value;
|
|
2352
2465
|
const setter = value?.type === 'AssignmentPattern' ? value.left : value;
|
|
2353
2466
|
if ((key === 1 || key === '1') && setter?.type === 'Identifier') {
|
|
2354
|
-
bind(setter,
|
|
2467
|
+
bind(setter, stateTuple.setter);
|
|
2355
2468
|
} else if (key === 0 || key === '0') {
|
|
2356
2469
|
bindSnapshotPattern(value, stateTuple.snapshot, bind);
|
|
2357
2470
|
}
|
|
@@ -2366,7 +2479,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2366
2479
|
) {
|
|
2367
2480
|
bind(declaration.id, { kind: 'ref' });
|
|
2368
2481
|
} else if (declarationKind === 'const' && stateTupleUpdater(initial, scope)) {
|
|
2369
|
-
target.bindings.set(declaration.id.name,
|
|
2482
|
+
target.bindings.set(declaration.id.name, stateTupleUpdater(initial, scope));
|
|
2370
2483
|
} else if (declarationKind === 'const' && stateTupleGetter(initial, scope)) {
|
|
2371
2484
|
target.bindings.set(declaration.id.name, STATE_GETTER_BINDING);
|
|
2372
2485
|
} else if (snapshot !== null) {
|
|
@@ -2433,6 +2546,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2433
2546
|
} else if (snapshot !== null) {
|
|
2434
2547
|
bindSnapshotPattern(declaration.id, snapshot, bind);
|
|
2435
2548
|
}
|
|
2549
|
+
// Proven callable, tuple and option bindings must keep their original
|
|
2550
|
+
// semantics. Projection provenance fills only otherwise unknown values.
|
|
2551
|
+
if (
|
|
2552
|
+
stateTuple === null &&
|
|
2553
|
+
snapshot === null &&
|
|
2554
|
+
(declarationKind === 'const' || !isReassigned(declaration.id))
|
|
2555
|
+
) {
|
|
2556
|
+
const known =
|
|
2557
|
+
declaration.id?.type === 'Identifier' ? target.bindings.get(declaration.id.name) : null;
|
|
2558
|
+
if (known == null || known.kind === 'other') {
|
|
2559
|
+
const prop = propProjection(initial, scope);
|
|
2560
|
+
// A value mixing props and state keeps its state provenance so effect
|
|
2561
|
+
// reads still link to the writer; the prop flag keeps the eager
|
|
2562
|
+
// initializer check.
|
|
2563
|
+
const states =
|
|
2564
|
+
declaration.id?.type === 'Identifier' ? projectedStates(initial, scope) : null;
|
|
2565
|
+
if (states !== null) bind(declaration.id, { kind: 'derived-state', states, prop });
|
|
2566
|
+
else if (prop) bindPropPattern(declaration.id, bind);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2436
2569
|
}
|
|
2437
2570
|
|
|
2438
2571
|
function stateTupleBinding(expression, scope) {
|
|
@@ -2446,9 +2579,21 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2446
2579
|
if (!STATE_HOOKS.has(hook)) return null;
|
|
2447
2580
|
// A known array initializer is a narrow mutator check, not a judgment
|
|
2448
2581
|
// about arbitrary methods on object snapshots or opaque hook outputs.
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2582
|
+
let tuple = stateTuples.get(node);
|
|
2583
|
+
if (tuple === undefined) {
|
|
2584
|
+
tuple = {
|
|
2585
|
+
kind: 'state-tuple',
|
|
2586
|
+
snapshot: {
|
|
2587
|
+
kind: 'snapshot',
|
|
2588
|
+
state: node,
|
|
2589
|
+
array: hook === 'useState' && unwrap(node.arguments?.[0])?.type === 'ArrayExpression',
|
|
2590
|
+
},
|
|
2591
|
+
setter: { kind: 'setter', state: node },
|
|
2592
|
+
getter: STATE_GETTER_BINDING,
|
|
2593
|
+
};
|
|
2594
|
+
stateTuples.set(node, tuple);
|
|
2595
|
+
}
|
|
2596
|
+
return tuple;
|
|
2452
2597
|
}
|
|
2453
2598
|
|
|
2454
2599
|
function snapshotBinding(expression, scope) {
|
|
@@ -2463,7 +2608,12 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2463
2608
|
const key = staticPrimitiveValue(node.property, scope);
|
|
2464
2609
|
if (key === 0 || key === '0') return tuple.snapshot;
|
|
2465
2610
|
}
|
|
2466
|
-
|
|
2611
|
+
const parent = snapshotBinding(node.object, scope);
|
|
2612
|
+
return parent === null
|
|
2613
|
+
? null
|
|
2614
|
+
: parent.state
|
|
2615
|
+
? { kind: 'snapshot', state: parent.state }
|
|
2616
|
+
: SNAPSHOT_BINDING;
|
|
2467
2617
|
}
|
|
2468
2618
|
|
|
2469
2619
|
function bindDeclaration(declaration, declarationKind, scope, phase) {
|
|
@@ -2491,13 +2641,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2491
2641
|
|
|
2492
2642
|
function stateTupleUpdater(value, scope) {
|
|
2493
2643
|
const member = unwrap(value);
|
|
2494
|
-
if (member?.type !== 'MemberExpression' || member.computed !== true) return
|
|
2644
|
+
if (member?.type !== 'MemberExpression' || member.computed !== true) return null;
|
|
2495
2645
|
const property = staticPrimitiveValue(member.property, scope);
|
|
2496
|
-
return
|
|
2497
|
-
(
|
|
2498
|
-
|
|
2499
|
-
resolve(scope, unwrap(member.object).name)?.kind === 'state-tuple'
|
|
2500
|
-
);
|
|
2646
|
+
return property === 1 || property === '1'
|
|
2647
|
+
? (stateTupleBinding(member.object, scope)?.setter ?? null)
|
|
2648
|
+
: null;
|
|
2501
2649
|
}
|
|
2502
2650
|
|
|
2503
2651
|
function stateTupleGetter(value, scope) {
|
|
@@ -2786,8 +2934,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2786
2934
|
if (scopes === undefined) functions.set(callback.node, (scopes = new Set()));
|
|
2787
2935
|
scopes.add(callback.scope);
|
|
2788
2936
|
} else {
|
|
2789
|
-
const identity =
|
|
2790
|
-
callback.kind === 'setter' || callback.kind === 'getter' ? callback.kind : callback;
|
|
2937
|
+
const identity = callback.kind === 'getter' ? callback.kind : callback;
|
|
2791
2938
|
if (seen.has(identity)) return;
|
|
2792
2939
|
seen.add(identity);
|
|
2793
2940
|
}
|
|
@@ -2850,7 +2997,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2850
2997
|
const binding = resolve(scope, node.name);
|
|
2851
2998
|
return isCallableValue(binding) ? binding : null;
|
|
2852
2999
|
}
|
|
2853
|
-
|
|
3000
|
+
const setter = stateTupleUpdater(node, scope);
|
|
3001
|
+
if (setter !== null) return setter;
|
|
2854
3002
|
if (stateTupleGetter(node, scope)) return STATE_GETTER_BINDING;
|
|
2855
3003
|
if (node?.type === 'SequenceExpression') {
|
|
2856
3004
|
return callableValue(node.expressions?.[node.expressions.length - 1], scope);
|
|
@@ -2962,6 +3110,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2962
3110
|
isCallableValue(binding) ||
|
|
2963
3111
|
binding?.kind === 'ref' ||
|
|
2964
3112
|
binding?.kind === 'snapshot' ||
|
|
3113
|
+
binding?.kind === 'prop' ||
|
|
3114
|
+
binding?.kind === 'derived-state' ||
|
|
2965
3115
|
binding?.kind === 'state-tuple' ||
|
|
2966
3116
|
binding?.kind === 'constant' ||
|
|
2967
3117
|
binding?.kind === 'linked-key'
|
|
@@ -2982,7 +3132,14 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2982
3132
|
|
|
2983
3133
|
function argumentValues(args, scope) {
|
|
2984
3134
|
if (args?.some((argument) => argument.type === 'SpreadElement')) return null;
|
|
2985
|
-
return (args ?? []).map((argument) =>
|
|
3135
|
+
return (args ?? []).map((argument) => {
|
|
3136
|
+
const value = expressionBinding(argument, scope);
|
|
3137
|
+
// Prop-state intent belongs to the component's own initializer. A
|
|
3138
|
+
// helper's parameter is supplied by callers, independently of spelling.
|
|
3139
|
+
if (value?.kind === 'prop') return OTHER_BINDING;
|
|
3140
|
+
if (value?.kind === 'derived-state' && value.prop) return { ...value, prop: false };
|
|
3141
|
+
return value;
|
|
3142
|
+
});
|
|
2986
3143
|
}
|
|
2987
3144
|
|
|
2988
3145
|
function mergePrimitive(left, right) {
|
|
@@ -3129,6 +3286,316 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3129
3286
|
return result;
|
|
3130
3287
|
}
|
|
3131
3288
|
|
|
3289
|
+
function bindPropPattern(pattern, bind) {
|
|
3290
|
+
if (pattern?.type === 'Identifier') bind(pattern, PROP_BINDING);
|
|
3291
|
+
else if (pattern?.type === 'AssignmentPattern') bindPropPattern(pattern.left, bind);
|
|
3292
|
+
else if (pattern?.type === 'RestElement') bindPropPattern(pattern.argument, bind);
|
|
3293
|
+
else if (pattern?.type === 'ObjectPattern') {
|
|
3294
|
+
for (const property of pattern.properties ?? [])
|
|
3295
|
+
bindPropPattern(property.value ?? property.argument, bind);
|
|
3296
|
+
} else if (pattern?.type === 'ArrayPattern') {
|
|
3297
|
+
for (const element of pattern.elements ?? []) bindPropPattern(element, bind);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function propProjection(expression, scope) {
|
|
3302
|
+
const node = unwrap(expression);
|
|
3303
|
+
if (node == null || FUNCTION_TYPES.has(node.type)) return false;
|
|
3304
|
+
if (node.type === 'Identifier') {
|
|
3305
|
+
const binding = resolve(scope, node.name);
|
|
3306
|
+
return binding?.kind === 'prop' || binding?.prop === true;
|
|
3307
|
+
}
|
|
3308
|
+
if (node.type === 'MemberExpression')
|
|
3309
|
+
return (
|
|
3310
|
+
propProjection(node.object, scope) ||
|
|
3311
|
+
(node.computed && propProjection(node.property, scope))
|
|
3312
|
+
);
|
|
3313
|
+
if (node.type === 'CallExpression' || node.type === 'NewExpression')
|
|
3314
|
+
return (
|
|
3315
|
+
(unwrap(node.callee)?.type === 'MemberExpression' &&
|
|
3316
|
+
propProjection(unwrap(node.callee).object, scope)) ||
|
|
3317
|
+
(node.arguments ?? []).some((argument) => propProjection(argument, scope))
|
|
3318
|
+
);
|
|
3319
|
+
if (node.type === 'Property')
|
|
3320
|
+
return (
|
|
3321
|
+
propProjection(node.value, scope) || (node.computed && propProjection(node.key, scope))
|
|
3322
|
+
);
|
|
3323
|
+
if (node.type === 'ObjectExpression')
|
|
3324
|
+
return (node.properties ?? []).some((property) => propProjection(property, scope));
|
|
3325
|
+
if (node.type === 'ArrayExpression')
|
|
3326
|
+
return (node.elements ?? []).some((element) => propProjection(element, scope));
|
|
3327
|
+
if (node.type === 'TemplateLiteral')
|
|
3328
|
+
return (node.expressions ?? []).some((value) => propProjection(value, scope));
|
|
3329
|
+
if (node.type === 'SequenceExpression') return propProjection(node.expressions?.at(-1), scope);
|
|
3330
|
+
if (node.type === 'ConditionalExpression')
|
|
3331
|
+
return (
|
|
3332
|
+
propProjection(node.test, scope) ||
|
|
3333
|
+
propProjection(node.consequent, scope) ||
|
|
3334
|
+
propProjection(node.alternate, scope)
|
|
3335
|
+
);
|
|
3336
|
+
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression')
|
|
3337
|
+
return propProjection(node.left, scope) || propProjection(node.right, scope);
|
|
3338
|
+
if (node.type === 'UnaryExpression' || node.type === 'SpreadElement')
|
|
3339
|
+
return propProjection(node.argument, scope);
|
|
3340
|
+
return false;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function projectedStates(expression, scope, states = null) {
|
|
3344
|
+
const node = unwrap(expression);
|
|
3345
|
+
if (node == null || typeof node !== 'object' || FUNCTION_TYPES.has(node.type)) return states;
|
|
3346
|
+
if (node.type === 'Identifier') {
|
|
3347
|
+
const binding = resolve(scope, node.name);
|
|
3348
|
+
if (binding?.state && binding.kind === 'snapshot') (states ??= new Set()).add(binding.state);
|
|
3349
|
+
if (binding?.kind === 'derived-state') {
|
|
3350
|
+
for (const state of binding.states) (states ??= new Set()).add(state);
|
|
3351
|
+
}
|
|
3352
|
+
return states;
|
|
3353
|
+
}
|
|
3354
|
+
if (node.type === 'MemberExpression') {
|
|
3355
|
+
const snapshot = snapshotBinding(node, scope);
|
|
3356
|
+
if (snapshot?.state) (states ??= new Set()).add(snapshot.state);
|
|
3357
|
+
states = projectedStates(node.object, scope, states);
|
|
3358
|
+
return node.computed ? projectedStates(node.property, scope, states) : states;
|
|
3359
|
+
}
|
|
3360
|
+
if (node.type === 'Property') {
|
|
3361
|
+
states = projectedStates(node.value, scope, states);
|
|
3362
|
+
return node.computed ? projectedStates(node.key, scope, states) : states;
|
|
3363
|
+
}
|
|
3364
|
+
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
|
|
3365
|
+
if (unwrap(node.callee)?.type === 'MemberExpression')
|
|
3366
|
+
states = projectedStates(unwrap(node.callee).object, scope, states);
|
|
3367
|
+
for (const argument of node.arguments ?? [])
|
|
3368
|
+
states = projectedStates(argument, scope, states);
|
|
3369
|
+
return states;
|
|
3370
|
+
}
|
|
3371
|
+
if (Array.isArray(node)) {
|
|
3372
|
+
for (const child of node) states = projectedStates(child, scope, states);
|
|
3373
|
+
} else if (!node.type?.startsWith('TS')) {
|
|
3374
|
+
for (const key in node) {
|
|
3375
|
+
if (!SKIP_KEYS.has(key) && !key.startsWith('_octane'))
|
|
3376
|
+
states = projectedStates(node[key], scope, states);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return states;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
function fetchFunction(expression, scope) {
|
|
3383
|
+
const node = unwrap(expression);
|
|
3384
|
+
if (node?.type === 'Identifier') {
|
|
3385
|
+
const value = resolve(scope, node.name);
|
|
3386
|
+
return value === FETCH_BINDING || (node.name === 'fetch' && value === null);
|
|
3387
|
+
}
|
|
3388
|
+
if (node?.type !== 'MemberExpression') return false;
|
|
3389
|
+
const object = ambientReference(node.object, scope);
|
|
3390
|
+
return (
|
|
3391
|
+
(object === GLOBAL_OBJECT_BINDING || object === AMBIENT_GLOBAL_BINDINGS.get('window')) &&
|
|
3392
|
+
ambientPropertyKey(node, scope) === 'fetch'
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function fetchRequest(expression, scope) {
|
|
3397
|
+
const node = unwrap(expression);
|
|
3398
|
+
if (node?.type === 'Identifier') return resolve(scope, node.name)?.request ?? null;
|
|
3399
|
+
if (node?.type !== 'CallExpression') return null;
|
|
3400
|
+
if (fetchFunction(node.callee, scope)) return node;
|
|
3401
|
+
const callee = unwrap(node.callee);
|
|
3402
|
+
if (callee?.type !== 'MemberExpression') return null;
|
|
3403
|
+
const method = ambientPropertyKey(callee, scope);
|
|
3404
|
+
return method === 'then' || method === 'catch' || method === 'finally'
|
|
3405
|
+
? fetchRequest(callee.object, scope)
|
|
3406
|
+
: null;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
function visitEffect(node, scope) {
|
|
3410
|
+
const enclosingEffect = currentEffect;
|
|
3411
|
+
const enclosingContinuation = fetchContinuation;
|
|
3412
|
+
const enclosingOwnsWrites = effectOwnsWrites;
|
|
3413
|
+
const callback = callableValue(node.arguments?.[0], scope);
|
|
3414
|
+
const record = {
|
|
3415
|
+
node,
|
|
3416
|
+
owner: renderOwner,
|
|
3417
|
+
callback,
|
|
3418
|
+
reads: new Set(),
|
|
3419
|
+
writes: new Set(),
|
|
3420
|
+
started: new Set(),
|
|
3421
|
+
cleanup: false,
|
|
3422
|
+
fetchUpdater: null,
|
|
3423
|
+
};
|
|
3424
|
+
currentEffect = record;
|
|
3425
|
+
fetchContinuation = false;
|
|
3426
|
+
effectOwnsWrites = true;
|
|
3427
|
+
effectRecords.push(record);
|
|
3428
|
+
try {
|
|
3429
|
+
visitExplicitStateDependencies(node.arguments?.[1], scope, record.reads);
|
|
3430
|
+
visitCallable(callback, unwrap(node.arguments?.[0]), 'effect');
|
|
3431
|
+
} finally {
|
|
3432
|
+
currentEffect = enclosingEffect;
|
|
3433
|
+
fetchContinuation = enclosingContinuation;
|
|
3434
|
+
effectOwnsWrites = enclosingOwnsWrites;
|
|
3435
|
+
}
|
|
3436
|
+
if (record.fetchUpdater !== null && !record.cleanup) {
|
|
3437
|
+
report(
|
|
3438
|
+
STRONG_EFFECT_DATA_FETCH,
|
|
3439
|
+
record.fetchUpdater,
|
|
3440
|
+
'Strong mode does not allow fetching data into state from an effect without cleanup. Read data with use() or a query binding; keep effects for external synchronization with cleanup.',
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function visitExplicitStateDependencies(expression, scope, dependencies) {
|
|
3446
|
+
const node = unwrap(expression);
|
|
3447
|
+
if (node?.type === 'ArrayExpression') {
|
|
3448
|
+
for (const element of node.elements ?? [])
|
|
3449
|
+
visitExplicitStateDependencies(element, scope, dependencies);
|
|
3450
|
+
} else {
|
|
3451
|
+
const states = projectedStates(node, scope);
|
|
3452
|
+
if (states !== null) for (const state of states) dependencies.add(state);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// Keep abrupt completions distinct until their owning statement consumes them.
|
|
3457
|
+
// A break exits a switch/loop, while a return exits the surrounding callback.
|
|
3458
|
+
function statementCompletions(statement, loopLabel = null) {
|
|
3459
|
+
if (statement == null) return NORMAL_COMPLETIONS;
|
|
3460
|
+
const known = loopLabel === null ? effectBranchExits.get(statement) : undefined;
|
|
3461
|
+
if (known !== undefined) return known;
|
|
3462
|
+
let completions;
|
|
3463
|
+
switch (statement.type) {
|
|
3464
|
+
case 'ReturnStatement':
|
|
3465
|
+
completions = new Set(['return']);
|
|
3466
|
+
break;
|
|
3467
|
+
case 'ThrowStatement':
|
|
3468
|
+
completions = new Set(['throw']);
|
|
3469
|
+
break;
|
|
3470
|
+
case 'BreakStatement':
|
|
3471
|
+
case 'ContinueStatement':
|
|
3472
|
+
completions = new Set([
|
|
3473
|
+
`${statement.type === 'BreakStatement' ? 'break' : 'continue'}:${statement.label?.name ?? ''}`,
|
|
3474
|
+
]);
|
|
3475
|
+
break;
|
|
3476
|
+
case 'BlockStatement':
|
|
3477
|
+
completions = statementsCompletions(statement.body);
|
|
3478
|
+
break;
|
|
3479
|
+
case 'IfStatement':
|
|
3480
|
+
completions = new Set([
|
|
3481
|
+
...statementCompletions(statement.consequent),
|
|
3482
|
+
...statementCompletions(statement.alternate),
|
|
3483
|
+
]);
|
|
3484
|
+
break;
|
|
3485
|
+
case 'SwitchStatement': {
|
|
3486
|
+
completions = new Set();
|
|
3487
|
+
let suffix = new Set(['normal']);
|
|
3488
|
+
let hasDefault = false;
|
|
3489
|
+
for (let index = statement.cases.length - 1; index >= 0; index--) {
|
|
3490
|
+
const branch = statement.cases[index];
|
|
3491
|
+
hasDefault ||= branch.test === null;
|
|
3492
|
+
const branchCompletions = statementsCompletions(branch.consequent);
|
|
3493
|
+
if (branchCompletions.delete('normal'))
|
|
3494
|
+
for (const completion of suffix) branchCompletions.add(completion);
|
|
3495
|
+
suffix = branchCompletions;
|
|
3496
|
+
for (const completion of suffix) completions.add(completion);
|
|
3497
|
+
}
|
|
3498
|
+
const breaks = completions.delete('break:');
|
|
3499
|
+
if (!hasDefault || breaks) completions.add('normal');
|
|
3500
|
+
break;
|
|
3501
|
+
}
|
|
3502
|
+
case 'ForStatement':
|
|
3503
|
+
case 'ForInStatement':
|
|
3504
|
+
case 'ForOfStatement':
|
|
3505
|
+
case 'WhileStatement':
|
|
3506
|
+
case 'DoWhileStatement': {
|
|
3507
|
+
const test = unwrap(statement.test);
|
|
3508
|
+
const indefinite =
|
|
3509
|
+
(statement.type === 'ForStatement' && test == null) ||
|
|
3510
|
+
(test?.type === 'Literal' && test.value === true);
|
|
3511
|
+
const neverStarts =
|
|
3512
|
+
statement.type !== 'DoWhileStatement' && test?.type === 'Literal' && test.value === false;
|
|
3513
|
+
completions = neverStarts
|
|
3514
|
+
? new Set(['normal'])
|
|
3515
|
+
: new Set(statementCompletions(statement.body));
|
|
3516
|
+
const repeats = completions.delete('normal');
|
|
3517
|
+
let continues = completions.delete('continue:');
|
|
3518
|
+
if (loopLabel !== null)
|
|
3519
|
+
continues = completions.delete(`continue:${loopLabel}`) || continues;
|
|
3520
|
+
const breaks = completions.delete('break:');
|
|
3521
|
+
if (
|
|
3522
|
+
breaks ||
|
|
3523
|
+
(!indefinite && (statement.type !== 'DoWhileStatement' || repeats || continues))
|
|
3524
|
+
)
|
|
3525
|
+
completions.add('normal');
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
case 'LabeledStatement':
|
|
3529
|
+
completions = new Set(statementCompletions(statement.body, statement.label.name));
|
|
3530
|
+
if (completions.delete(`break:${statement.label.name}`)) completions.add('normal');
|
|
3531
|
+
break;
|
|
3532
|
+
case 'TryStatement': {
|
|
3533
|
+
completions = new Set(statementCompletions(statement.block));
|
|
3534
|
+
if (statement.handler != null) {
|
|
3535
|
+
completions.delete('throw');
|
|
3536
|
+
for (const completion of statementCompletions(statement.handler.body))
|
|
3537
|
+
completions.add(completion);
|
|
3538
|
+
}
|
|
3539
|
+
if (statement.finalizer != null) {
|
|
3540
|
+
const finalizer = statementCompletions(statement.finalizer);
|
|
3541
|
+
if (!finalizer.has('normal')) completions.clear();
|
|
3542
|
+
for (const completion of finalizer)
|
|
3543
|
+
if (completion !== 'normal') completions.add(completion);
|
|
3544
|
+
}
|
|
3545
|
+
break;
|
|
3546
|
+
}
|
|
3547
|
+
default:
|
|
3548
|
+
return NORMAL_COMPLETIONS;
|
|
3549
|
+
}
|
|
3550
|
+
if (loopLabel === null) effectBranchExits.set(statement, completions);
|
|
3551
|
+
return completions;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
function statementsCompletions(statements) {
|
|
3555
|
+
const completions = new Set(['normal']);
|
|
3556
|
+
for (const statement of statements ?? []) {
|
|
3557
|
+
if (!completions.delete('normal')) break;
|
|
3558
|
+
for (const completion of statementCompletions(statement)) completions.add(completion);
|
|
3559
|
+
}
|
|
3560
|
+
return completions;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
function branchAlwaysExits(statement) {
|
|
3564
|
+
return !statementCompletions(statement).has('normal');
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
// A returned value counts as cleanup unless it is provably not callable: a
|
|
3568
|
+
// fetch request or a known non-function value.
|
|
3569
|
+
function markEffectCleanup(argument, scope) {
|
|
3570
|
+
// Async callbacks return a Promise, even when their resolved value is a function.
|
|
3571
|
+
if (currentFunctionIsAsync) return;
|
|
3572
|
+
const value = unwrap(argument);
|
|
3573
|
+
if (value?.type === 'CallExpression') {
|
|
3574
|
+
const callback = callableValue(value.callee, scope);
|
|
3575
|
+
const callbacks =
|
|
3576
|
+
callback?.kind === 'callback-choice' && callback.complete ? callback.values : [callback];
|
|
3577
|
+
if (
|
|
3578
|
+
callbacks.length > 0 &&
|
|
3579
|
+
callbacks.every((value) => value?.kind === 'callback' && value.node.async)
|
|
3580
|
+
)
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
if (
|
|
3584
|
+
value != null &&
|
|
3585
|
+
(callableValue(value, scope) !== null ||
|
|
3586
|
+
(staticExpressionValue(value, scope) === UNKNOWN_VALUE &&
|
|
3587
|
+
fetchRequest(value, scope) === null))
|
|
3588
|
+
) {
|
|
3589
|
+
currentEffect.cleanup = true;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
function effectCallbackIsCurrent(value) {
|
|
3594
|
+
return value?.kind === 'callback'
|
|
3595
|
+
? value.node === currentFunction
|
|
3596
|
+
: value?.kind === 'callback-choice' && value.values.some(effectCallbackIsCurrent);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3132
3599
|
function visitCallable(value, origin, phase, args = null) {
|
|
3133
3600
|
if (value?.kind === 'callback') {
|
|
3134
3601
|
visitCallback(value.node, value.scope, phase, args);
|
|
@@ -3136,9 +3603,23 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3136
3603
|
for (const callback of value.values) visitCallable(callback, origin, phase, args);
|
|
3137
3604
|
} else if (value?.kind === 'effect-event') {
|
|
3138
3605
|
if (phase === 'render') reportEffectEventCall(origin);
|
|
3139
|
-
else if (phase === 'effect'
|
|
3140
|
-
|
|
3141
|
-
|
|
3606
|
+
else if (phase === 'effect' || currentEffect !== null) {
|
|
3607
|
+
const enclosingCollectEffectReads = collectEffectReads;
|
|
3608
|
+
// Effect Events observe current values without subscribing the calling effect.
|
|
3609
|
+
// Their writes still participate in fetch and cross-effect state flow checks.
|
|
3610
|
+
collectEffectReads = false;
|
|
3611
|
+
try {
|
|
3612
|
+
visitCallable(value.callback, origin, phase, args);
|
|
3613
|
+
} finally {
|
|
3614
|
+
collectEffectReads = enclosingCollectEffectReads;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
} else if (value?.kind === 'setter') {
|
|
3618
|
+
if (currentEffect !== null && effectOwnsWrites && value.state) {
|
|
3619
|
+
currentEffect.writes.add(value.state);
|
|
3620
|
+
if (fetchContinuation) currentEffect.fetchUpdater ??= origin;
|
|
3621
|
+
}
|
|
3622
|
+
if (phase === 'render' || phase === 'effect') reportSetter(origin, phase);
|
|
3142
3623
|
} else if (value?.kind === 'getter' && phase === 'render' && currentFunctionChecksRenderReads) {
|
|
3143
3624
|
reportStateGetterCall(origin);
|
|
3144
3625
|
}
|
|
@@ -3324,18 +3805,75 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3324
3805
|
return;
|
|
3325
3806
|
}
|
|
3326
3807
|
if (node.type?.startsWith('TS')) return;
|
|
3808
|
+
if (
|
|
3809
|
+
currentEffect !== null &&
|
|
3810
|
+
EFFECT_CONTROL_TYPES.has(node.type) &&
|
|
3811
|
+
currentEffectControl?.node !== node
|
|
3812
|
+
) {
|
|
3813
|
+
const parent = currentEffectControl;
|
|
3814
|
+
const control = {
|
|
3815
|
+
node,
|
|
3816
|
+
parent,
|
|
3817
|
+
loop: node.type !== 'SwitchStatement' && node.type !== 'LabeledStatement',
|
|
3818
|
+
label:
|
|
3819
|
+
node.type === 'LabeledStatement'
|
|
3820
|
+
? node.label.name
|
|
3821
|
+
: parent?.node.type === 'LabeledStatement' && parent.node.body === node
|
|
3822
|
+
? parent.label
|
|
3823
|
+
: null,
|
|
3824
|
+
breakContinuation: false,
|
|
3825
|
+
continueContinuation: false,
|
|
3826
|
+
};
|
|
3827
|
+
const before = fetchContinuation;
|
|
3828
|
+
currentEffectControl = control;
|
|
3829
|
+
try {
|
|
3830
|
+
visit(node, scope, phase, readAccess);
|
|
3831
|
+
if (fetchContinuation && control.loop && branchAlwaysExits(node.body))
|
|
3832
|
+
fetchContinuation = before;
|
|
3833
|
+
// Break and continue leave the current branch, but their asynchronous
|
|
3834
|
+
// path can still reach an update after the target loop or switch.
|
|
3835
|
+
fetchContinuation ||= control.breakContinuation || control.continueContinuation;
|
|
3836
|
+
} finally {
|
|
3837
|
+
currentEffectControl = parent;
|
|
3838
|
+
}
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
templatePolicy.visit(node, scope, readAccess);
|
|
3327
3842
|
|
|
3328
3843
|
switch (node.type) {
|
|
3329
3844
|
case 'ImportDeclaration':
|
|
3330
3845
|
case 'MetaProperty':
|
|
3331
|
-
case 'BreakStatement':
|
|
3332
|
-
case 'ContinueStatement':
|
|
3333
3846
|
case 'JSXIdentifier':
|
|
3334
3847
|
case 'Literal':
|
|
3335
3848
|
case 'ThisExpression':
|
|
3336
3849
|
case 'Super':
|
|
3337
3850
|
return;
|
|
3851
|
+
case 'BreakStatement':
|
|
3852
|
+
case 'ContinueStatement': {
|
|
3853
|
+
if (currentEffect !== null && fetchContinuation) {
|
|
3854
|
+
const continuation = node.type === 'ContinueStatement';
|
|
3855
|
+
for (let control = currentEffectControl; control !== null; control = control.parent) {
|
|
3856
|
+
if (
|
|
3857
|
+
node.label
|
|
3858
|
+
? control.label !== node.label.name || (continuation && !control.loop)
|
|
3859
|
+
: continuation
|
|
3860
|
+
? !control.loop
|
|
3861
|
+
: control.node.type === 'LabeledStatement'
|
|
3862
|
+
)
|
|
3863
|
+
continue;
|
|
3864
|
+
control[continuation ? 'continueContinuation' : 'breakContinuation'] = true;
|
|
3865
|
+
break;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3338
3870
|
case 'Identifier':
|
|
3871
|
+
if (readAccess && collectEffectReads && currentEffect !== null) {
|
|
3872
|
+
const binding = resolve(scope, node.name);
|
|
3873
|
+
if (binding?.kind === 'snapshot' && binding.state) currentEffect.reads.add(binding.state);
|
|
3874
|
+
if (binding?.kind === 'derived-state')
|
|
3875
|
+
for (const state of binding.states) currentEffect.reads.add(state);
|
|
3876
|
+
}
|
|
3339
3877
|
if (
|
|
3340
3878
|
readAccess &&
|
|
3341
3879
|
phase === 'render' &&
|
|
@@ -3405,9 +3943,33 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3405
3943
|
return;
|
|
3406
3944
|
case 'FunctionDeclaration':
|
|
3407
3945
|
case 'FunctionExpression':
|
|
3408
|
-
case 'ArrowFunctionExpression':
|
|
3409
|
-
|
|
3946
|
+
case 'ArrowFunctionExpression': {
|
|
3947
|
+
// A declaration does not execute a callback. Calls and callbacks
|
|
3948
|
+
// passed to external work are visited separately with the effect.
|
|
3949
|
+
const enclosingEffect = currentEffect;
|
|
3950
|
+
currentEffect = null;
|
|
3951
|
+
try {
|
|
3952
|
+
visitFunction(node, scope, phase === 'module' ? 'render' : 'deferred');
|
|
3953
|
+
} finally {
|
|
3954
|
+
currentEffect = enclosingEffect;
|
|
3955
|
+
}
|
|
3410
3956
|
return;
|
|
3957
|
+
}
|
|
3958
|
+
case 'AwaitExpression': {
|
|
3959
|
+
visit(node.argument, scope, phase);
|
|
3960
|
+
if (currentEffect !== null) {
|
|
3961
|
+
const request = fetchRequest(node.argument, scope);
|
|
3962
|
+
if (request !== null && currentEffect.started.has(request)) fetchContinuation = true;
|
|
3963
|
+
}
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
case 'ReturnStatement': {
|
|
3967
|
+
if (currentEffect !== null && effectCallbackIsCurrent(currentEffect.callback)) {
|
|
3968
|
+
markEffectCleanup(node.argument, scope);
|
|
3969
|
+
}
|
|
3970
|
+
visit(node.argument, scope, phase);
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3411
3973
|
case 'ClassDeclaration':
|
|
3412
3974
|
case 'ClassExpression': {
|
|
3413
3975
|
const classScope = createScope(scope, 'block', [], node.id ? [node.id] : []);
|
|
@@ -3435,20 +3997,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3435
3997
|
if (node.render) visit(node.render, block, executionPhase);
|
|
3436
3998
|
return;
|
|
3437
3999
|
}
|
|
3438
|
-
case 'IfStatement':
|
|
3439
|
-
visit(node.test, scope, phase);
|
|
3440
|
-
const branchPhase = phaseAfter(node.test, phase);
|
|
3441
|
-
const branches = conditionalExpressionBranches(node, scope);
|
|
3442
|
-
if ((branches & 1) !== 0) visit(node.consequent, scope, branchPhase);
|
|
3443
|
-
if ((branches & 2) !== 0) visit(node.alternate, scope, branchPhase);
|
|
3444
|
-
return;
|
|
3445
|
-
}
|
|
4000
|
+
case 'IfStatement':
|
|
3446
4001
|
case 'ConditionalExpression': {
|
|
3447
4002
|
visit(node.test, scope, phase);
|
|
3448
4003
|
const branchPhase = phaseAfter(node.test, phase);
|
|
3449
4004
|
const branches = conditionalExpressionBranches(node, scope);
|
|
4005
|
+
const before = fetchContinuation;
|
|
3450
4006
|
if ((branches & 1) !== 0) visit(node.consequent, scope, branchPhase);
|
|
4007
|
+
const consequent =
|
|
4008
|
+
(branches & 1) !== 0 &&
|
|
4009
|
+
fetchContinuation &&
|
|
4010
|
+
(node.type !== 'IfStatement' || !branchAlwaysExits(node.consequent));
|
|
4011
|
+
fetchContinuation = before;
|
|
3451
4012
|
if ((branches & 2) !== 0) visit(node.alternate, scope, branchPhase);
|
|
4013
|
+
// An abrupt branch cannot reach a later state update. Inspect all writes
|
|
4014
|
+
// inside it (including finally) before excluding its continuation.
|
|
4015
|
+
fetchContinuation =
|
|
4016
|
+
consequent ||
|
|
4017
|
+
((branches & 2) !== 0 &&
|
|
4018
|
+
fetchContinuation &&
|
|
4019
|
+
(node.type !== 'IfStatement' || !branchAlwaysExits(node.alternate)));
|
|
3452
4020
|
return;
|
|
3453
4021
|
}
|
|
3454
4022
|
case 'LogicalExpression':
|
|
@@ -3545,11 +4113,41 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3545
4113
|
reportAmbientRead(node);
|
|
3546
4114
|
}
|
|
3547
4115
|
}
|
|
4116
|
+
if (readAccess && collectEffectReads && currentEffect !== null) {
|
|
4117
|
+
const snapshot = snapshotBinding(node, scope);
|
|
4118
|
+
if (snapshot?.state) currentEffect.reads.add(snapshot.state);
|
|
4119
|
+
}
|
|
3548
4120
|
return;
|
|
3549
4121
|
}
|
|
3550
4122
|
case 'LabeledStatement':
|
|
3551
4123
|
visit(node.body, scope, phase);
|
|
3552
4124
|
return;
|
|
4125
|
+
case 'TryStatement': {
|
|
4126
|
+
let pendingControls = null;
|
|
4127
|
+
if (
|
|
4128
|
+
currentEffect !== null &&
|
|
4129
|
+
currentEffectControl !== null &&
|
|
4130
|
+
branchAlwaysExits(node.finalizer)
|
|
4131
|
+
) {
|
|
4132
|
+
pendingControls = [];
|
|
4133
|
+
for (let control = currentEffectControl; control !== null; control = control.parent)
|
|
4134
|
+
pendingControls.push([
|
|
4135
|
+
control,
|
|
4136
|
+
control.breakContinuation,
|
|
4137
|
+
control.continueContinuation,
|
|
4138
|
+
]);
|
|
4139
|
+
}
|
|
4140
|
+
visit(node.block, scope, phase);
|
|
4141
|
+
visit(node.handler, scope, phase);
|
|
4142
|
+
// An abrupt finally replaces pending exits from try/catch. Restore the
|
|
4143
|
+
// earlier paths before recording any new exits from finally itself.
|
|
4144
|
+
for (const [control, breaks, continues] of pendingControls ?? []) {
|
|
4145
|
+
control.breakContinuation = breaks;
|
|
4146
|
+
control.continueContinuation = continues;
|
|
4147
|
+
}
|
|
4148
|
+
visit(node.finalizer, scope, phase);
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
3553
4151
|
case 'JSXSwitchExpression': {
|
|
3554
4152
|
visit(node.discriminant, scope, phase);
|
|
3555
4153
|
let searchPhase = phaseAfter(node.discriminant, phase);
|
|
@@ -3583,7 +4181,10 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3583
4181
|
const fallbackPhase = searchPhase;
|
|
3584
4182
|
searchPhase = initialPhase;
|
|
3585
4183
|
let fallthroughPhase = null;
|
|
4184
|
+
const matchingContinuation = fetchContinuation;
|
|
4185
|
+
let fallthroughContinuation = false;
|
|
3586
4186
|
for (const branch of branches) {
|
|
4187
|
+
fetchContinuation = matchingContinuation || fallthroughContinuation;
|
|
3587
4188
|
if (currentFunctionIsAsync && searchPhase !== 'deferred' && alwaysAwaits(branch.test)) {
|
|
3588
4189
|
searchPhase = 'deferred';
|
|
3589
4190
|
}
|
|
@@ -3594,6 +4195,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3594
4195
|
? 'deferred'
|
|
3595
4196
|
: phase;
|
|
3596
4197
|
const executionPhase = visitStatements(branch.consequent, switchScope, branchPhase);
|
|
4198
|
+
fallthroughContinuation = fetchContinuation && !branch.consequent.some(branchAlwaysExits);
|
|
3597
4199
|
const last = branch.consequent?.[branch.consequent.length - 1];
|
|
3598
4200
|
fallthroughPhase =
|
|
3599
4201
|
last?.type === 'BreakStatement' ||
|
|
@@ -3603,6 +4205,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3603
4205
|
? null
|
|
3604
4206
|
: executionPhase;
|
|
3605
4207
|
}
|
|
4208
|
+
fetchContinuation = matchingContinuation || fallthroughContinuation;
|
|
3606
4209
|
return;
|
|
3607
4210
|
}
|
|
3608
4211
|
case 'VariableDeclaration': {
|
|
@@ -3665,10 +4268,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3665
4268
|
visitExplicitDependencies(node.arguments?.[dependencyIndex], scope);
|
|
3666
4269
|
}
|
|
3667
4270
|
if (EFFECT_HOOKS.has(hook)) {
|
|
3668
|
-
|
|
4271
|
+
visitEffect(node, scope);
|
|
3669
4272
|
return;
|
|
3670
4273
|
}
|
|
4274
|
+
if (
|
|
4275
|
+
hook === 'useReducer' &&
|
|
4276
|
+
executionPhase === 'render' &&
|
|
4277
|
+
(node.arguments?.[2] == null ||
|
|
4278
|
+
staticExpressionValue(node.arguments[2], scope) === UNDEFINED_VALUE) &&
|
|
4279
|
+
propProjection(node.arguments?.[1], scope)
|
|
4280
|
+
)
|
|
4281
|
+
unlinkedPropInitializers.add(unwrap(node.arguments[1]));
|
|
3671
4282
|
if (hook === 'useState' || hook === 'useMemo') {
|
|
4283
|
+
if (
|
|
4284
|
+
hook === 'useState' &&
|
|
4285
|
+
executionPhase === 'render' &&
|
|
4286
|
+
propProjection(node.arguments?.[0], scope) &&
|
|
4287
|
+
callableValue(node.arguments?.[0], scope) === null
|
|
4288
|
+
) {
|
|
4289
|
+
unlinkedPropInitializers.add(unwrap(node.arguments[0]));
|
|
4290
|
+
}
|
|
3672
4291
|
visitSynchronousHookCallback(
|
|
3673
4292
|
node.arguments?.[0],
|
|
3674
4293
|
scope,
|
|
@@ -3717,9 +4336,31 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3717
4336
|
}
|
|
3718
4337
|
}
|
|
3719
4338
|
}
|
|
4339
|
+
if (currentEffect !== null && hook === null) {
|
|
4340
|
+
if (effectOwnsWrites && fetchFunction(callee, scope)) currentEffect.started.add(node);
|
|
4341
|
+
const callback = callableValue(callee, scope);
|
|
4342
|
+
if (callback === null) {
|
|
4343
|
+
const promiseContinuation =
|
|
4344
|
+
callee?.type === 'MemberExpression' &&
|
|
4345
|
+
['then', 'catch', 'finally'].includes(ambientPropertyKey(callee, scope));
|
|
4346
|
+
const request = promiseContinuation ? fetchRequest(callee.object, scope) : null;
|
|
4347
|
+
const before = fetchContinuation;
|
|
4348
|
+
const beforeOwnsWrites = effectOwnsWrites;
|
|
4349
|
+
effectOwnsWrites &&= promiseContinuation;
|
|
4350
|
+
if (request !== null && currentEffect.started.has(request)) fetchContinuation = true;
|
|
4351
|
+
try {
|
|
4352
|
+
for (const argument of node.arguments ?? [])
|
|
4353
|
+
visitCallable(callableValue(argument, scope), unwrap(argument), 'deferred');
|
|
4354
|
+
} finally {
|
|
4355
|
+
fetchContinuation = before;
|
|
4356
|
+
effectOwnsWrites = beforeOwnsWrites;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
3720
4360
|
if (
|
|
3721
4361
|
executionPhase === 'render' ||
|
|
3722
4362
|
executionPhase === 'effect' ||
|
|
4363
|
+
currentEffect !== null ||
|
|
3723
4364
|
FUNCTION_TYPES.has(callee?.type)
|
|
3724
4365
|
) {
|
|
3725
4366
|
const callback = callableValue(callee, scope);
|
|
@@ -3917,6 +4558,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3917
4558
|
executionPhase = 'deferred';
|
|
3918
4559
|
}
|
|
3919
4560
|
visit(node.body, loop, executionPhase);
|
|
4561
|
+
if (currentEffectControl?.node === node)
|
|
4562
|
+
fetchContinuation ||= currentEffectControl.continueContinuation;
|
|
3920
4563
|
visit(node.update, loop, executionPhase);
|
|
3921
4564
|
return;
|
|
3922
4565
|
}
|
|
@@ -3934,12 +4577,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3934
4577
|
addPatternNames(declaration.id, row.bindings, OTHER_BINDING);
|
|
3935
4578
|
}
|
|
3936
4579
|
}
|
|
3937
|
-
addPatternNames(node.index, row.bindings,
|
|
4580
|
+
addPatternNames(node.index, row.bindings, { kind: 'other', strongIndex: true });
|
|
3938
4581
|
visit(node.left, row, executionPhase);
|
|
3939
4582
|
const enclosingRetainedRowScope = currentRetainedRowScope;
|
|
3940
4583
|
currentRetainedRowScope = row;
|
|
3941
4584
|
try {
|
|
3942
|
-
|
|
4585
|
+
templatePolicy.enterKey(node.key, row);
|
|
4586
|
+
try {
|
|
4587
|
+
visit(node.key, row, executionPhase);
|
|
4588
|
+
} finally {
|
|
4589
|
+
templatePolicy.exitKey();
|
|
4590
|
+
}
|
|
3943
4591
|
visit(node.body, row, executionPhase);
|
|
3944
4592
|
} finally {
|
|
3945
4593
|
currentRetainedRowScope = enclosingRetainedRowScope;
|
|
@@ -3966,6 +4614,9 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3966
4614
|
: phase;
|
|
3967
4615
|
if (node.left?.type === 'ArrayPattern' || node.left?.type === 'ObjectPattern') {
|
|
3968
4616
|
visitPatternExpressions(node.left, loop, executionPhase);
|
|
4617
|
+
} else if (node.left?.type === 'VariableDeclaration') {
|
|
4618
|
+
for (const declaration of node.left.declarations)
|
|
4619
|
+
visitPatternExpressions(declaration.id, loop, executionPhase);
|
|
3969
4620
|
} else {
|
|
3970
4621
|
visit(node.left, loop, executionPhase, false);
|
|
3971
4622
|
}
|
|
@@ -3983,6 +4634,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
3983
4634
|
}
|
|
3984
4635
|
case 'DoWhileStatement': {
|
|
3985
4636
|
visit(node.body, scope, phase);
|
|
4637
|
+
if (currentEffectControl?.node === node)
|
|
4638
|
+
fetchContinuation ||= currentEffectControl.continueContinuation;
|
|
3986
4639
|
const executionPhase =
|
|
3987
4640
|
currentFunctionIsAsync &&
|
|
3988
4641
|
phase !== 'deferred' &&
|
|
@@ -4001,17 +4654,62 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
4001
4654
|
}
|
|
4002
4655
|
|
|
4003
4656
|
for (const statement of ast.body ?? []) visit(statement, moduleScope, 'module');
|
|
4657
|
+
for (const initial of unlinkedPropInitializers) {
|
|
4658
|
+
report(
|
|
4659
|
+
STRONG_UNLINKED_PROP_STATE,
|
|
4660
|
+
initial,
|
|
4661
|
+
'Strong mode requires explicit intent for prop-derived state. Use useLinkedState(value, () => value) to follow the prop, or a lazy useState initializer (or useReducer third initializer) to capture only its initial value.',
|
|
4662
|
+
);
|
|
4663
|
+
}
|
|
4664
|
+
// Match state identity, not its local spelling. The graph is local to one
|
|
4665
|
+
// analyzed module and compares only distinct effects in the same render root.
|
|
4666
|
+
const effectWriters = new Map();
|
|
4667
|
+
for (const effect of effectRecords) {
|
|
4668
|
+
for (const state of effect.writes) {
|
|
4669
|
+
let writers = effectWriters.get(state);
|
|
4670
|
+
if (writers === undefined) effectWriters.set(state, (writers = []));
|
|
4671
|
+
writers.push(effect);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
for (const effect of effectRecords) {
|
|
4675
|
+
if (
|
|
4676
|
+
[...effect.reads].some((state) =>
|
|
4677
|
+
effectWriters
|
|
4678
|
+
.get(state)
|
|
4679
|
+
?.some((writer) => writer.node !== effect.node && writer.owner === effect.owner),
|
|
4680
|
+
)
|
|
4681
|
+
) {
|
|
4682
|
+
report(
|
|
4683
|
+
STRONG_EFFECT_CHAIN,
|
|
4684
|
+
effect.node,
|
|
4685
|
+
'Strong mode does not allow an effect to depend on state written by another effect in the same component. Derive the value during render, use useLinkedState, or keep the external synchronization in one effect.',
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4004
4689
|
if (hasNestedCodeBlock) {
|
|
4005
|
-
diagnostics.push(...strongLocalityDiagnostics(ast, source, filename, isReassigned));
|
|
4690
|
+
diagnostics.push(...strongLocalityDiagnostics(ast, source, filename, isReassigned, hookNames));
|
|
4691
|
+
}
|
|
4692
|
+
templatePolicy.finish();
|
|
4693
|
+
for (const policy of analyzeStrongHookPolicies(ast, options)) {
|
|
4694
|
+
diagnostics.push({
|
|
4695
|
+
...diagnostic(policy.code, filename, policy.node, policy.message),
|
|
4696
|
+
severity: policy.severity,
|
|
4697
|
+
});
|
|
4006
4698
|
}
|
|
4007
|
-
|
|
4699
|
+
diagnostics.push(...analyzeStrongHTML(ast, source, filename, options));
|
|
4700
|
+
const nativeChangeAnalysis = analyzeNativeChangeDiagnostics(ast, source, filename, {
|
|
4701
|
+
...options,
|
|
4702
|
+
strong: true,
|
|
4703
|
+
});
|
|
4704
|
+
diagnostics.push(...nativeChangeAnalysis.diagnostics);
|
|
4705
|
+
return { enabled, diagnostics, nativeChangeAnalysis, strongHookAnalysis };
|
|
4008
4706
|
}
|
|
4009
4707
|
|
|
4010
4708
|
/** Throw the first Strong-mode violation using the original authored location. */
|
|
4011
4709
|
export function assertStrongMode(ast, source, filename, options) {
|
|
4012
4710
|
if (options?.strong !== true && !source.includes('use strong')) return;
|
|
4013
4711
|
const result = analyzeStrongMode(ast, source, filename, options);
|
|
4014
|
-
const violation = result.diagnostics
|
|
4712
|
+
const violation = result.diagnostics.find((item) => item.severity === 'error');
|
|
4015
4713
|
if (violation === undefined) return result;
|
|
4016
4714
|
const { code, message } = violation;
|
|
4017
4715
|
const { line, column } = violation.start;
|