octane 0.2.7 → 0.2.8

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.
@@ -53,9 +53,98 @@ const NO_RETURN_VALUE = Symbol('no return value');
53
53
  const OTHER_BINDING = { kind: 'other' };
54
54
  const SNAPSHOT_BINDING = { kind: 'snapshot' };
55
55
  const ARRAY_SNAPSHOT_BINDING = { kind: 'snapshot', array: true };
56
- const STATE_TUPLE_BINDING = { kind: 'state-tuple', snapshot: SNAPSHOT_BINDING };
57
- const ARRAY_STATE_TUPLE_BINDING = { kind: 'state-tuple', snapshot: ARRAY_SNAPSHOT_BINDING };
56
+ const STATE_GETTER_BINDING = { kind: 'getter' };
57
+ const STATE_TUPLE_BINDING = {
58
+ kind: 'state-tuple',
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
+ };
58
67
  const UNDEFINED_BINDING = { kind: 'constant', value: UNDEFINED_VALUE, primitive: undefined };
68
+ const GLOBAL_OBJECT_BINDING = { kind: 'global-object' };
69
+ const AMBIENT_GLOBAL_BINDINGS = new Map(
70
+ [
71
+ 'window',
72
+ 'document',
73
+ 'localStorage',
74
+ 'sessionStorage',
75
+ 'navigator',
76
+ 'location',
77
+ 'matchMedia',
78
+ ].map((name) => [name, { kind: 'ambient', name }]),
79
+ );
80
+ // Reading a standard language builtin is permitted here. Its calls still
81
+ // follow the separate clock/randomness rules; this is not a purity whitelist.
82
+ const LANGUAGE_GLOBALS = new Set([
83
+ 'undefined',
84
+ 'NaN',
85
+ 'Infinity',
86
+ 'globalThis',
87
+ 'Object',
88
+ 'Function',
89
+ 'Boolean',
90
+ 'Symbol',
91
+ 'Error',
92
+ 'AggregateError',
93
+ 'EvalError',
94
+ 'RangeError',
95
+ 'ReferenceError',
96
+ 'SyntaxError',
97
+ 'TypeError',
98
+ 'URIError',
99
+ 'Number',
100
+ 'BigInt',
101
+ 'Math',
102
+ 'Date',
103
+ 'String',
104
+ 'RegExp',
105
+ 'Array',
106
+ 'Int8Array',
107
+ 'Uint8Array',
108
+ 'Uint8ClampedArray',
109
+ 'Int16Array',
110
+ 'Uint16Array',
111
+ 'Int32Array',
112
+ 'Uint32Array',
113
+ 'BigInt64Array',
114
+ 'BigUint64Array',
115
+ 'Float16Array',
116
+ 'Float32Array',
117
+ 'Float64Array',
118
+ 'Map',
119
+ 'Set',
120
+ 'WeakMap',
121
+ 'WeakSet',
122
+ 'ArrayBuffer',
123
+ 'SharedArrayBuffer',
124
+ 'DataView',
125
+ 'Atomics',
126
+ 'JSON',
127
+ 'Promise',
128
+ 'Reflect',
129
+ 'Proxy',
130
+ 'WeakRef',
131
+ 'FinalizationRegistry',
132
+ 'Intl',
133
+ 'Iterator',
134
+ 'AsyncIterator',
135
+ 'DisposableStack',
136
+ 'AsyncDisposableStack',
137
+ 'isFinite',
138
+ 'isNaN',
139
+ 'parseFloat',
140
+ 'parseInt',
141
+ 'decodeURI',
142
+ 'decodeURIComponent',
143
+ 'encodeURI',
144
+ 'encodeURIComponent',
145
+ 'escape',
146
+ 'unescape',
147
+ ]);
59
148
  const SKIP_KEYS = new Set([
60
149
  'type',
61
150
  'start',
@@ -72,8 +161,12 @@ const SKIP_KEYS = new Set([
72
161
  ]);
73
162
 
74
163
  export const STRONG_RENDER_STATE_UPDATE = 'OCTANE_STRONG_RENDER_STATE_UPDATE';
164
+ export const STRONG_RENDER_STATE_GETTER_CALL = 'OCTANE_STRONG_RENDER_STATE_GETTER_CALL';
165
+ export const STRONG_RENDER_MODULE_STATE_READ = 'OCTANE_STRONG_RENDER_MODULE_STATE_READ';
166
+ export const STRONG_RENDER_AMBIENT_READ = 'OCTANE_STRONG_RENDER_AMBIENT_READ';
75
167
  export const STRONG_EFFECT_STATE_UPDATE = 'OCTANE_STRONG_EFFECT_STATE_UPDATE';
76
168
  export const STRONG_RENDER_REF_WRITE = 'OCTANE_STRONG_RENDER_REF_WRITE';
169
+ export const STRONG_RENDER_REF_READ = 'OCTANE_STRONG_RENDER_REF_READ';
77
170
  export const STRONG_RENDER_SNAPSHOT_MUTATION = 'OCTANE_STRONG_RENDER_SNAPSHOT_MUTATION';
78
171
  export const STRONG_RETAINED_ROW_MUTATION = 'OCTANE_STRONG_RETAINED_ROW_MUTATION';
79
172
  export const STRONG_RENDER_IMPURE_CALL = 'OCTANE_STRONG_RENDER_IMPURE_CALL';
@@ -1200,6 +1293,40 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1200
1293
  return reassignedBindings.has(identifier);
1201
1294
  }
1202
1295
  const moduleScope = createScope(null, 'module', ast.body ?? [], [], isReassigned);
1296
+ const mutableModuleDeclarations = new Map();
1297
+ const reassignedModuleNames = new Map();
1298
+ const moduleAmbientAliases = new Map();
1299
+ const activeAmbientAliases = new Set();
1300
+ function recordModuleAmbientAliases(pattern, initializer, properties = []) {
1301
+ if (pattern?.type === 'Identifier') {
1302
+ moduleAmbientAliases.set(pattern.name, { initializer, properties });
1303
+ } else if (pattern?.type === 'AssignmentPattern') {
1304
+ recordModuleAmbientAliases(pattern.left, initializer, properties);
1305
+ } else if (pattern?.type === 'ObjectPattern') {
1306
+ for (const property of pattern.properties ?? []) {
1307
+ if (property.type === 'Property') {
1308
+ recordModuleAmbientAliases(property.value, initializer, [...properties, property]);
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ function recordMutableModulePattern(pattern) {
1314
+ if (pattern?.type === 'Identifier') {
1315
+ let declarations = mutableModuleDeclarations.get(pattern.name);
1316
+ if (declarations === undefined) {
1317
+ mutableModuleDeclarations.set(pattern.name, (declarations = []));
1318
+ }
1319
+ declarations.push(pattern);
1320
+ } else if (pattern?.type === 'AssignmentPattern' || pattern?.type === 'RestElement') {
1321
+ recordMutableModulePattern(pattern.left ?? pattern.argument);
1322
+ } else if (pattern?.type === 'ArrayPattern') {
1323
+ for (const element of pattern.elements ?? []) recordMutableModulePattern(element);
1324
+ } else if (pattern?.type === 'ObjectPattern') {
1325
+ for (const property of pattern.properties ?? []) {
1326
+ recordMutableModulePattern(property.argument ?? property.value);
1327
+ }
1328
+ }
1329
+ }
1203
1330
  const activeCallbacks = new Set();
1204
1331
  const activeReturnCallbacks = new Set();
1205
1332
  const callResults = new WeakMap();
@@ -1251,7 +1378,23 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1251
1378
  for (const statement of ast.body ?? []) {
1252
1379
  const declaration = declarationOf(statement);
1253
1380
  if (declaration?.type === 'VariableDeclaration') {
1381
+ if (declaration.kind === 'let' && declaration.declare !== true) {
1382
+ for (const binding of declaration.declarations ?? []) {
1383
+ recordMutableModulePattern(binding.id);
1384
+ }
1385
+ }
1254
1386
  for (const binding of declaration.declarations ?? []) {
1387
+ const initial = unwrap(binding.init);
1388
+ if (
1389
+ declaration.kind === 'const' &&
1390
+ (initial?.type === 'Identifier' ||
1391
+ initial?.type === 'MemberExpression' ||
1392
+ initial?.type === 'Literal' ||
1393
+ initial?.type === 'TemplateLiteral' ||
1394
+ initial?.type === 'BinaryExpression')
1395
+ ) {
1396
+ recordModuleAmbientAliases(binding.id, initial);
1397
+ }
1255
1398
  if (binding.id?.type === 'Identifier') {
1256
1399
  addNamedRenderRoot(unwrap(binding.init), binding.id.name);
1257
1400
  }
@@ -1266,6 +1409,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1266
1409
  let returnCycles = 0;
1267
1410
  let currentFunctionIsAsync = false;
1268
1411
  let currentFunctionChecksImpureCalls = false;
1412
+ let currentFunctionChecksRenderReads = false;
1269
1413
  let currentRetainedRowScope = null;
1270
1414
 
1271
1415
  function predeclareHoistedVars(node, scope) {
@@ -1296,6 +1440,9 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1296
1440
  if (value.type === 'VariableDeclaration' && value.kind === 'var') {
1297
1441
  for (const declaration of value.declarations ?? []) {
1298
1442
  addPatternNames(declaration.id, names, OTHER_BINDING);
1443
+ if (scope === moduleScope && value.declare !== true) {
1444
+ recordMutableModulePattern(declaration.id);
1445
+ }
1299
1446
  }
1300
1447
  }
1301
1448
  for (const key in value) {
@@ -1328,6 +1475,170 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1328
1475
  report(code, node, message, [{ hook: 'useLinkedState' }]);
1329
1476
  }
1330
1477
 
1478
+ function reportStateGetterCall(node) {
1479
+ report(
1480
+ STRONG_RENDER_STATE_GETTER_CALL,
1481
+ node,
1482
+ 'Strong mode does not allow calling a state getter during render. Render from the state tuple snapshot; call the getter in an event, effect, or deferred callback for the latest scheduled state.',
1483
+ );
1484
+ }
1485
+
1486
+ function reportModuleStateRead(node) {
1487
+ report(
1488
+ STRONG_RENDER_MODULE_STATE_READ,
1489
+ node,
1490
+ 'Strong mode does not allow reading a reassigned module variable during render. Move the value into state or context, or pass an immutable snapshot as a prop.',
1491
+ );
1492
+ }
1493
+
1494
+ function reportAmbientRead(node) {
1495
+ report(
1496
+ STRONG_RENDER_AMBIENT_READ,
1497
+ node,
1498
+ 'Strong mode does not allow reading browser globals during render. Use useSyncExternalStore with a server snapshot for live browser state, or read it in an effect or a lazy state initializer. Lazy initializers still need to handle server rendering.',
1499
+ );
1500
+ }
1501
+
1502
+ function ambientMemberBinding(object, key) {
1503
+ if (object === GLOBAL_OBJECT_BINDING || object === AMBIENT_GLOBAL_BINDINGS.get('window')) {
1504
+ if (key === 'globalThis') return GLOBAL_OBJECT_BINDING;
1505
+ return AMBIENT_GLOBAL_BINDINGS.get(key) ?? null;
1506
+ }
1507
+ return null;
1508
+ }
1509
+
1510
+ function ambientKeyValue(expression, scope) {
1511
+ const node = unwrap(expression);
1512
+ if (node?.type !== 'Identifier' || resolveScope(scope, node.name) !== moduleScope)
1513
+ return UNKNOWN_PRIMITIVE;
1514
+ const alias = moduleAmbientAliases.get(node.name);
1515
+ if (
1516
+ alias === undefined ||
1517
+ alias.properties.length !== 0 ||
1518
+ activeAmbientAliases.has(node.name)
1519
+ ) {
1520
+ return UNKNOWN_PRIMITIVE;
1521
+ }
1522
+ // A render root can appear before the module constant used as its key.
1523
+ activeAmbientAliases.add(node.name);
1524
+ try {
1525
+ return staticPrimitiveValue(alias.initializer, moduleScope, true);
1526
+ } finally {
1527
+ activeAmbientAliases.delete(node.name);
1528
+ }
1529
+ }
1530
+
1531
+ function ambientPropertyKey(property, scope) {
1532
+ const key = property.type === 'Property' ? property.key : property.property;
1533
+ return property.computed ? staticPrimitiveValue(key, scope, true) : (key?.name ?? key?.value);
1534
+ }
1535
+
1536
+ function ambientReference(expression, scope) {
1537
+ const node = unwrap(expression);
1538
+ if (node?.type === 'Identifier' || node?.type === 'JSXIdentifier') {
1539
+ const binding = resolve(scope, node.name);
1540
+ if (binding?.kind === 'ambient' || binding === GLOBAL_OBJECT_BINDING) return binding;
1541
+ if (binding === null) {
1542
+ return node.name === 'globalThis'
1543
+ ? GLOBAL_OBJECT_BINDING
1544
+ : (AMBIENT_GLOBAL_BINDINGS.get(node.name) ?? null);
1545
+ }
1546
+ const alias = moduleAmbientAliases.get(node.name);
1547
+ if (
1548
+ alias !== undefined &&
1549
+ resolveScope(scope, node.name) === moduleScope &&
1550
+ !activeAmbientAliases.has(node.name)
1551
+ ) {
1552
+ // Module functions can precede their const aliases. Resolve only
1553
+ // browser handles, never taint a copied scalar module snapshot.
1554
+ activeAmbientAliases.add(node.name);
1555
+ try {
1556
+ let value = ambientReference(alias.initializer, moduleScope);
1557
+ for (const property of alias.properties) {
1558
+ value = ambientMemberBinding(value, ambientPropertyKey(property, moduleScope));
1559
+ }
1560
+ return value;
1561
+ } finally {
1562
+ activeAmbientAliases.delete(node.name);
1563
+ }
1564
+ }
1565
+ return null;
1566
+ }
1567
+ if (node?.type === 'MemberExpression' || node?.type === 'JSXMemberExpression') {
1568
+ const object = ambientReference(node.object, scope);
1569
+ return object === GLOBAL_OBJECT_BINDING || object === AMBIENT_GLOBAL_BINDINGS.get('window')
1570
+ ? ambientMemberBinding(object, ambientPropertyKey(node, scope))
1571
+ : null;
1572
+ }
1573
+ return null;
1574
+ }
1575
+
1576
+ function isAmbientMemberRead(node, scope) {
1577
+ const value = unwrap(node);
1578
+ if (value?.type !== 'MemberExpression' && value?.type !== 'JSXMemberExpression') return false;
1579
+ return (
1580
+ ambientReference(value.object, scope) === GLOBAL_OBJECT_BINDING &&
1581
+ !LANGUAGE_GLOBALS.has(ambientPropertyKey(value, scope))
1582
+ );
1583
+ }
1584
+
1585
+ function bindAmbientPattern(pattern, value, scope, bind) {
1586
+ if (value !== GLOBAL_OBJECT_BINDING && value?.kind !== 'ambient') return;
1587
+ if (pattern?.type === 'Identifier') {
1588
+ bind(pattern, value);
1589
+ } else if (pattern?.type === 'AssignmentPattern') {
1590
+ bindAmbientPattern(pattern.left, value, scope, bind);
1591
+ } else if (pattern?.type === 'ObjectPattern') {
1592
+ for (const property of pattern.properties ?? []) {
1593
+ if (property.type !== 'Property') continue;
1594
+ bindAmbientPattern(
1595
+ property.value,
1596
+ ambientMemberBinding(value, ambientPropertyKey(property, scope)),
1597
+ scope,
1598
+ bind,
1599
+ );
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ function mutableModuleRead(name, scope) {
1605
+ const declarations = mutableModuleDeclarations.get(name);
1606
+ if (declarations === undefined || resolveScope(scope, name) !== moduleScope) {
1607
+ return false;
1608
+ }
1609
+ let reassigned = reassignedModuleNames.get(name);
1610
+ if (reassigned === undefined) {
1611
+ reassigned = declarations.some(isReassigned);
1612
+ reassignedModuleNames.set(name, reassigned);
1613
+ }
1614
+ return reassigned;
1615
+ }
1616
+
1617
+ function definitelySkippedOptionalContinuation(expression, scope) {
1618
+ let node = expression;
1619
+ while (node != null) {
1620
+ // Grouping ends the chain; the caller decides whether a grouped
1621
+ // undefined value is itself behind an optional access.
1622
+ if (node.type === 'ChainExpression') return false;
1623
+ if (TRANSPARENT_EXPRESSIONS.has(node.type)) {
1624
+ node = node.expression;
1625
+ continue;
1626
+ }
1627
+ if (node.type !== 'CallExpression' && node.type !== 'MemberExpression') return false;
1628
+ const target = node.type === 'CallExpression' ? node.callee : node.object;
1629
+ if (node.optional === true && definitelyNullishOptionalValue(target, scope)) return true;
1630
+ node = target;
1631
+ }
1632
+ return false;
1633
+ }
1634
+
1635
+ function definitelyNullishOptionalValue(expression, scope) {
1636
+ return (
1637
+ (staticExpressionValue(expression, scope) & ~NULLISH_VALUE) === 0 ||
1638
+ definitelySkippedOptionalContinuation(unwrap(expression), scope)
1639
+ );
1640
+ }
1641
+
1331
1642
  function reportRef(node) {
1332
1643
  report(
1333
1644
  STRONG_RENDER_REF_WRITE,
@@ -1336,6 +1647,32 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1336
1647
  );
1337
1648
  }
1338
1649
 
1650
+ function reportRefRead(node) {
1651
+ report(
1652
+ STRONG_RENDER_REF_READ,
1653
+ node,
1654
+ 'Strong mode does not allow reading useRef.current during render. Read the ref in an event or effect, or use state or useLinkedState for values that drive render output.',
1655
+ );
1656
+ }
1657
+
1658
+ function isRefObject(value, scope) {
1659
+ const object = unwrap(value);
1660
+ if (object?.type === 'Identifier') return resolve(scope, object.name)?.kind === 'ref';
1661
+ if (object?.type === 'SequenceExpression') {
1662
+ return isRefObject(object.expressions?.[object.expressions.length - 1], scope);
1663
+ }
1664
+ return object?.type === 'CallExpression' && importedHook(object.callee, scope) === 'useRef';
1665
+ }
1666
+
1667
+ function readCurrentRef(member, scope) {
1668
+ const value = unwrap(member);
1669
+ if (value?.type !== 'MemberExpression') return false;
1670
+ const isCurrent = value.computed
1671
+ ? staticPrimitiveValue(value.property, scope) === 'current'
1672
+ : value.property?.type === 'Identifier' && value.property.name === 'current';
1673
+ return isCurrent && isRefObject(value.object, scope);
1674
+ }
1675
+
1339
1676
  function reportSnapshotMutation(node) {
1340
1677
  report(
1341
1678
  STRONG_RENDER_SNAPSHOT_MUTATION,
@@ -1770,6 +2107,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1770
2107
  }
1771
2108
 
1772
2109
  function definitelyDefined(value) {
2110
+ if (value === GLOBAL_OBJECT_BINDING) return true;
1773
2111
  if (value?.kind === 'constant' && value.primitive !== UNKNOWN_PRIMITIVE) {
1774
2112
  return value.primitive !== undefined;
1775
2113
  }
@@ -1784,6 +2122,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1784
2122
  function visitParameters(node, parentScope, phase, args) {
1785
2123
  const parameters = node.params ?? [];
1786
2124
  const parameterScope = createScope(parentScope, 'function', [], parameters);
2125
+ function bindGetter(identifier, binding) {
2126
+ if (identifier?.type === 'Identifier' && !isReassigned(identifier)) {
2127
+ parameterScope.bindings.set(identifier.name, binding);
2128
+ }
2129
+ }
1787
2130
  if (
1788
2131
  node.type === 'FunctionExpression' &&
1789
2132
  node.id?.name &&
@@ -1796,17 +2139,38 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1796
2139
  if (parameter.type === 'TSParameterProperty') parameter = parameter.parameter;
1797
2140
  let value = args === null ? OTHER_BINDING : (args[index] ?? UNDEFINED_BINDING);
1798
2141
  if (parameter.type === 'AssignmentPattern') {
1799
- visitPatternExpressions(parameter.left, parameterScope, phase);
1800
- if (!definitelyDefined(value)) visit(parameter.right, parameterScope, phase);
2142
+ const usesDefault = !definitelyDefined(value);
2143
+ if (usesDefault) visit(parameter.right, parameterScope, phase);
2144
+ const defaultValue = usesDefault
2145
+ ? expressionBinding(parameter.right, parameterScope)
2146
+ : null;
1801
2147
  value =
1802
2148
  value.kind === 'constant' && value.primitive === undefined
1803
- ? expressionBinding(parameter.right, parameterScope)
2149
+ ? defaultValue
1804
2150
  : definitelyDefined(value)
1805
2151
  ? value
1806
- : OTHER_BINDING;
2152
+ : defaultValue?.kind === 'getter' ||
2153
+ defaultValue?.kind === 'state-tuple' ||
2154
+ defaultValue === GLOBAL_OBJECT_BINDING ||
2155
+ defaultValue?.kind === 'ambient'
2156
+ ? defaultValue
2157
+ : OTHER_BINDING;
2158
+ visitPatternExpressions(
2159
+ parameter.left,
2160
+ parameterScope,
2161
+ usesDefault ? phaseAfter(parameter.right, phase) : phase,
2162
+ parameter.left.type === 'ObjectPattern' &&
2163
+ (value?.kind === 'ref' ||
2164
+ (usesDefault && isRefObject(parameter.right, parameterScope))),
2165
+ value,
2166
+ );
2167
+ bindStateGetterPattern(parameter.left, value, parameterScope, bindGetter);
2168
+ bindAmbientPattern(parameter.left, value, parameterScope, bindGetter);
1807
2169
  parameter = parameter.left;
1808
2170
  } else {
1809
- visitPatternExpressions(parameter, parameterScope, phase);
2171
+ visitPatternExpressions(parameter, parameterScope, phase, value?.kind === 'ref', value);
2172
+ bindStateGetterPattern(parameter, value, parameterScope, bindGetter);
2173
+ bindAmbientPattern(parameter, value, parameterScope, bindGetter);
1810
2174
  }
1811
2175
  if (parameter.type === 'Identifier' && !isReassigned(parameter)) {
1812
2176
  parameterScope.bindings.set(parameter.name, value);
@@ -1818,11 +2182,12 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1818
2182
  function visitFunction(node, parentScope, phase, args = null) {
1819
2183
  const enclosingFunctionIsAsync = currentFunctionIsAsync;
1820
2184
  const enclosingFunctionChecksImpureCalls = currentFunctionChecksImpureCalls;
2185
+ const enclosingFunctionChecksRenderReads = currentFunctionChecksRenderReads;
1821
2186
  currentFunctionIsAsync = node.async === true;
1822
2187
  if (phase === 'render' && !activeCallbacks.has(node)) {
1823
2188
  // Ordinary module helpers may be used only by events. Check their
1824
2189
  // standard calls when a known render root invokes them synchronously.
1825
- currentFunctionChecksImpureCalls =
2190
+ currentFunctionChecksImpureCalls = currentFunctionChecksRenderReads =
1826
2191
  renderRoots.has(node) || node.body?.type === 'JSXCodeBlock';
1827
2192
  }
1828
2193
  try {
@@ -1854,38 +2219,74 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1854
2219
  } finally {
1855
2220
  currentFunctionIsAsync = enclosingFunctionIsAsync;
1856
2221
  currentFunctionChecksImpureCalls = enclosingFunctionChecksImpureCalls;
2222
+ currentFunctionChecksRenderReads = enclosingFunctionChecksRenderReads;
1857
2223
  }
1858
2224
  }
1859
2225
 
1860
- function visitPatternExpressions(pattern, scope, phase) {
2226
+ function visitPatternExpressions(pattern, scope, phase, refSource = false, ambientSource = null) {
1861
2227
  let executionPhase = phase;
1862
2228
  if (pattern?.type === 'TSParameterProperty') {
1863
- return visitPatternExpressions(pattern.parameter, scope, phase);
2229
+ return visitPatternExpressions(pattern.parameter, scope, phase, refSource, ambientSource);
1864
2230
  } else if (pattern?.type === 'Identifier') {
1865
2231
  if (executionPhase === 'render') reportRetainedRowMutation(pattern, scope);
1866
2232
  } else if (pattern?.type === 'AssignmentPattern') {
1867
- executionPhase = visitPatternExpressions(pattern.left, scope, executionPhase);
2233
+ executionPhase = visitPatternExpressions(
2234
+ pattern.left,
2235
+ scope,
2236
+ executionPhase,
2237
+ refSource,
2238
+ ambientSource,
2239
+ );
1868
2240
  visit(pattern.right, scope, executionPhase);
1869
2241
  } else if (pattern?.type === 'ArrayPattern') {
1870
2242
  for (const element of pattern.elements ?? []) {
1871
2243
  executionPhase = visitPatternExpressions(element, scope, executionPhase);
1872
2244
  }
1873
2245
  } else if (pattern?.type === 'ObjectPattern') {
2246
+ let currentExcluded = false;
1874
2247
  for (const property of pattern.properties ?? []) {
1875
2248
  if (property.computed) {
1876
2249
  visit(property.key, scope, executionPhase);
1877
2250
  executionPhase = phaseAfter(property.key, executionPhase);
1878
2251
  }
2252
+ if (
2253
+ ambientSource === GLOBAL_OBJECT_BINDING &&
2254
+ executionPhase === 'render' &&
2255
+ currentFunctionChecksImpureCalls &&
2256
+ (property.type === 'RestElement' ||
2257
+ !LANGUAGE_GLOBALS.has(ambientPropertyKey(property, scope)))
2258
+ ) {
2259
+ reportAmbientRead(property.type === 'RestElement' ? property : property.key);
2260
+ }
2261
+ const currentProperty =
2262
+ refSource &&
2263
+ property.type === 'Property' &&
2264
+ (property.computed
2265
+ ? staticPrimitiveValue(property.key, scope)
2266
+ : (property.key?.name ?? property.key?.value)) === 'current';
2267
+ if (
2268
+ refSource &&
2269
+ executionPhase === 'render' &&
2270
+ currentFunctionChecksRenderReads &&
2271
+ (currentProperty || (property.type === 'RestElement' && !currentExcluded))
2272
+ ) {
2273
+ reportRefRead(currentProperty ? property.key : property);
2274
+ }
2275
+ if (currentProperty) currentExcluded = true;
1879
2276
  executionPhase = visitPatternExpressions(
1880
2277
  property.argument ?? property.value,
1881
2278
  scope,
1882
2279
  executionPhase,
2280
+ false,
2281
+ ambientSource === null
2282
+ ? null
2283
+ : ambientMemberBinding(ambientSource, ambientPropertyKey(property, scope)),
1883
2284
  );
1884
2285
  }
1885
2286
  } else if (pattern?.type === 'RestElement') {
1886
2287
  executionPhase = visitPatternExpressions(pattern.argument, scope, executionPhase);
1887
2288
  } else if (pattern?.type === 'MemberExpression') {
1888
- visit(pattern, scope, executionPhase);
2289
+ visit(pattern, scope, executionPhase, false);
1889
2290
  executionPhase = phaseAfter(pattern, executionPhase, true);
1890
2291
  if (executionPhase === 'render') {
1891
2292
  reportSnapshotWrite(pattern, scope);
@@ -1895,6 +2296,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1895
2296
  return executionPhase;
1896
2297
  }
1897
2298
 
2299
+ function bindStateGetterPattern(pattern, tuple, scope, bind) {
2300
+ if (tuple?.kind !== 'state-tuple') return;
2301
+ if (pattern?.type === 'ArrayPattern') {
2302
+ const element = pattern.elements?.[2];
2303
+ bind(element?.type === 'AssignmentPattern' ? element.left : element, tuple.getter);
2304
+ } else if (pattern?.type === 'ObjectPattern') {
2305
+ for (const property of pattern.properties ?? []) {
2306
+ if (property.type !== 'Property') continue;
2307
+ const key = property.computed
2308
+ ? staticPrimitiveValue(property.key, scope)
2309
+ : property.key?.type === 'Identifier'
2310
+ ? property.key.name
2311
+ : property.key?.value;
2312
+ if (key !== 2 && key !== '2') continue;
2313
+ const value = property.value;
2314
+ bind(value?.type === 'AssignmentPattern' ? value.left : value, tuple.getter);
2315
+ }
2316
+ }
2317
+ }
2318
+
1898
2319
  function bindDeclarationValue(declaration, declarationKind, scope) {
1899
2320
  const target = declarationScope(scope, declarationKind);
1900
2321
  const initial = unwrap(declaration.init);
@@ -1905,6 +2326,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1905
2326
  declarationKind === 'const' || !isReassigned(identifier) ? value : OTHER_BINDING,
1906
2327
  );
1907
2328
  }
2329
+ const ambient = ambientReference(initial, scope);
2330
+ if (ambient !== null) {
2331
+ bindAmbientPattern(declaration.id, ambient, scope, bind);
2332
+ return;
2333
+ }
1908
2334
  const stateTuple = stateTupleBinding(initial, scope);
1909
2335
  const snapshot =
1910
2336
  stateTuple === null && declarationKind === 'const' ? snapshotBinding(initial, scope) : null;
@@ -1913,6 +2339,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1913
2339
  const element = declaration.id.elements?.[1];
1914
2340
  const setter = element?.type === 'AssignmentPattern' ? element.left : element;
1915
2341
  bind(setter, { kind: 'setter' });
2342
+ bindStateGetterPattern(declaration.id, stateTuple, scope, bind);
1916
2343
  } else if (declaration.id?.type === 'ObjectPattern' && stateTuple) {
1917
2344
  for (const property of declaration.id.properties ?? []) {
1918
2345
  if (property.type !== 'Property') continue;
@@ -1929,6 +2356,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1929
2356
  bindSnapshotPattern(value, stateTuple.snapshot, bind);
1930
2357
  }
1931
2358
  }
2359
+ bindStateGetterPattern(declaration.id, stateTuple, scope, bind);
1932
2360
  } else if (declaration.id?.type === 'Identifier') {
1933
2361
  if (stateTuple) {
1934
2362
  bind(declaration.id, stateTuple);
@@ -1939,12 +2367,15 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1939
2367
  bind(declaration.id, { kind: 'ref' });
1940
2368
  } else if (declarationKind === 'const' && stateTupleUpdater(initial, scope)) {
1941
2369
  target.bindings.set(declaration.id.name, { kind: 'setter' });
2370
+ } else if (declarationKind === 'const' && stateTupleGetter(initial, scope)) {
2371
+ target.bindings.set(declaration.id.name, STATE_GETTER_BINDING);
1942
2372
  } else if (snapshot !== null) {
1943
2373
  target.bindings.set(declaration.id.name, snapshot);
1944
2374
  } else if (declarationKind === 'const' && initial?.type === 'Identifier') {
1945
2375
  const value = resolve(scope, initial.name);
1946
2376
  if (
1947
2377
  value?.kind === 'setter' ||
2378
+ value?.kind === 'getter' ||
1948
2379
  value?.kind === 'ref' ||
1949
2380
  value?.kind === 'callback' ||
1950
2381
  value?.kind === 'callback-choice' ||
@@ -2038,7 +2469,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2038
2469
  function bindDeclaration(declaration, declarationKind, scope, phase) {
2039
2470
  bindDeclarationValue(declaration, declarationKind, scope);
2040
2471
  visit(declaration.init, scope, phase);
2041
- return visitPatternExpressions(declaration.id, scope, phaseAfter(declaration.init, phase));
2472
+ return visitPatternExpressions(
2473
+ declaration.id,
2474
+ scope,
2475
+ phaseAfter(declaration.init, phase),
2476
+ declaration.id?.type === 'ObjectPattern' && isRefObject(declaration.init, scope),
2477
+ declaration.id?.type === 'ObjectPattern' ? ambientReference(declaration.init, scope) : null,
2478
+ );
2042
2479
  }
2043
2480
 
2044
2481
  function visitCallback(node, parentScope, phase, args = null) {
@@ -2063,7 +2500,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2063
2500
  );
2064
2501
  }
2065
2502
 
2066
- function staticPrimitiveValue(value, scope) {
2503
+ function stateTupleGetter(value, scope) {
2504
+ const member = unwrap(value);
2505
+ if (member?.type !== 'MemberExpression' || member.computed !== true) return false;
2506
+ const key = staticPrimitiveValue(member.property, scope);
2507
+ return (
2508
+ (key === 2 || key === '2') &&
2509
+ stateTupleBinding(member.object, scope)?.getter === STATE_GETTER_BINDING
2510
+ );
2511
+ }
2512
+
2513
+ function staticPrimitiveValue(value, scope, moduleConstantKeys = false) {
2067
2514
  const expression = unwrap(value);
2068
2515
  if (expression?.type === 'CallExpression') {
2069
2516
  const result = callResult(expression, scope);
@@ -2075,7 +2522,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2075
2522
  if (binding?.kind === 'linked-key') return binding.value;
2076
2523
  if (binding?.kind === 'constant') return binding.primitive;
2077
2524
  if (binding == null && expression.name === 'undefined') return undefined;
2078
- return UNKNOWN_PRIMITIVE;
2525
+ return moduleConstantKeys ? ambientKeyValue(expression, scope) : UNKNOWN_PRIMITIVE;
2079
2526
  }
2080
2527
  if (expression?.type === 'TemplateLiteral') {
2081
2528
  let result = '';
@@ -2084,7 +2531,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2084
2531
  if (text == null) return UNKNOWN_PRIMITIVE;
2085
2532
  result += text;
2086
2533
  if (index < (expression.expressions?.length ?? 0)) {
2087
- const part = staticPrimitiveValue(expression.expressions[index], scope);
2534
+ const part = staticPrimitiveValue(
2535
+ expression.expressions[index],
2536
+ scope,
2537
+ moduleConstantKeys,
2538
+ );
2088
2539
  if (
2089
2540
  part === UNKNOWN_PRIMITIVE ||
2090
2541
  (part !== null && typeof part === 'object') ||
@@ -2099,8 +2550,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2099
2550
  return result;
2100
2551
  }
2101
2552
  if (expression?.type === 'BinaryExpression' && expression.operator === '+') {
2102
- const left = staticPrimitiveValue(expression.left, scope);
2103
- const right = staticPrimitiveValue(expression.right, scope);
2553
+ const left = staticPrimitiveValue(expression.left, scope, moduleConstantKeys);
2554
+ const right = staticPrimitiveValue(expression.right, scope, moduleConstantKeys);
2104
2555
  if (
2105
2556
  left === UNKNOWN_PRIMITIVE ||
2106
2557
  right === UNKNOWN_PRIMITIVE ||
@@ -2125,7 +2576,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2125
2576
  }
2126
2577
  if (expression?.type === 'UnaryExpression') {
2127
2578
  if (expression.operator === 'void') return undefined;
2128
- const argument = staticPrimitiveValue(expression.argument, scope);
2579
+ const argument = staticPrimitiveValue(expression.argument, scope, moduleConstantKeys);
2129
2580
  if (expression.operator === '!') {
2130
2581
  if (argument !== UNKNOWN_PRIMITIVE) return !argument;
2131
2582
  const value = staticExpressionValue(expression.argument, scope);
@@ -2151,22 +2602,22 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2151
2602
  }
2152
2603
  if (expression?.type === 'SequenceExpression') {
2153
2604
  const expressions = expression.expressions ?? [];
2154
- return staticPrimitiveValue(expressions[expressions.length - 1], scope);
2605
+ return staticPrimitiveValue(expressions[expressions.length - 1], scope, moduleConstantKeys);
2155
2606
  }
2156
2607
  if (expression?.type === 'ConditionalExpression') {
2157
2608
  const branches = conditionalExpressionBranches(expression, scope);
2158
2609
  return branches === 1
2159
- ? staticPrimitiveValue(expression.consequent, scope)
2610
+ ? staticPrimitiveValue(expression.consequent, scope, moduleConstantKeys)
2160
2611
  : branches === 2
2161
- ? staticPrimitiveValue(expression.alternate, scope)
2612
+ ? staticPrimitiveValue(expression.alternate, scope, moduleConstantKeys)
2162
2613
  : UNKNOWN_PRIMITIVE;
2163
2614
  }
2164
2615
  if (expression?.type === 'LogicalExpression') {
2165
2616
  const branches = logicalExpressionBranches(expression, scope);
2166
2617
  return branches === 1
2167
- ? staticPrimitiveValue(expression.left, scope)
2618
+ ? staticPrimitiveValue(expression.left, scope, moduleConstantKeys)
2168
2619
  : branches === 2
2169
- ? staticPrimitiveValue(expression.right, scope)
2620
+ ? staticPrimitiveValue(expression.right, scope, moduleConstantKeys)
2170
2621
  : UNKNOWN_PRIMITIVE;
2171
2622
  }
2172
2623
  return UNKNOWN_PRIMITIVE;
@@ -2188,7 +2639,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2188
2639
  FUNCTION_TYPES.has(expression?.type) ||
2189
2640
  expression?.type === 'ObjectExpression' ||
2190
2641
  expression?.type === 'ArrayExpression' ||
2191
- stateTupleUpdater(expression, scope)
2642
+ stateTupleUpdater(expression, scope) ||
2643
+ stateTupleGetter(expression, scope)
2192
2644
  ) {
2193
2645
  return TRUTHY_VALUE;
2194
2646
  } else if (
@@ -2207,6 +2659,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2207
2659
  binding?.kind === 'callback' ||
2208
2660
  binding?.kind === 'effect-event' ||
2209
2661
  binding?.kind === 'setter' ||
2662
+ binding?.kind === 'getter' ||
2210
2663
  binding?.kind === 'ref' ||
2211
2664
  binding?.kind === 'state-tuple' ||
2212
2665
  binding?.kind === 'linked-key' ||
@@ -2305,7 +2758,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2305
2758
  value?.kind === 'callback' ||
2306
2759
  value?.kind === 'callback-choice' ||
2307
2760
  value?.kind === 'effect-event' ||
2308
- value?.kind === 'setter'
2761
+ value?.kind === 'setter' ||
2762
+ value?.kind === 'getter'
2309
2763
  );
2310
2764
  }
2311
2765
 
@@ -2332,7 +2786,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2332
2786
  if (scopes === undefined) functions.set(callback.node, (scopes = new Set()));
2333
2787
  scopes.add(callback.scope);
2334
2788
  } else {
2335
- const identity = callback.kind === 'setter' ? 'setter' : callback;
2789
+ const identity =
2790
+ callback.kind === 'setter' || callback.kind === 'getter' ? callback.kind : callback;
2336
2791
  if (seen.has(identity)) return;
2337
2792
  seen.add(identity);
2338
2793
  }
@@ -2396,6 +2851,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2396
2851
  return isCallableValue(binding) ? binding : null;
2397
2852
  }
2398
2853
  if (stateTupleUpdater(node, scope)) return { kind: 'setter' };
2854
+ if (stateTupleGetter(node, scope)) return STATE_GETTER_BINDING;
2399
2855
  if (node?.type === 'SequenceExpression') {
2400
2856
  return callableValue(node.expressions?.[node.expressions.length - 1], scope);
2401
2857
  }
@@ -2498,6 +2954,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2498
2954
 
2499
2955
  function expressionBinding(expression, scope) {
2500
2956
  const node = unwrap(expression);
2957
+ const ambient = ambientReference(node, scope);
2958
+ if (ambient !== null) return ambient;
2501
2959
  if (node?.type === 'Identifier') {
2502
2960
  const binding = resolve(scope, node.name);
2503
2961
  if (
@@ -2681,12 +3139,14 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2681
3139
  else if (phase === 'effect') visitCallable(value.callback, origin, phase, args);
2682
3140
  } else if (value?.kind === 'setter' && (phase === 'render' || phase === 'effect')) {
2683
3141
  reportSetter(origin, phase);
3142
+ } else if (value?.kind === 'getter' && phase === 'render' && currentFunctionChecksRenderReads) {
3143
+ reportStateGetterCall(origin);
2684
3144
  }
2685
3145
  }
2686
3146
 
2687
3147
  function visitSynchronousHookCallback(value, scope, phase, stateInitializer = false) {
2688
3148
  const checkImpureCalls = currentFunctionChecksImpureCalls;
2689
- // Lazy state initialization may read a clock or randomness. Keep its
3149
+ // Lazy state initialization may read a clock, randomness, or browser state. Keep its
2690
3150
  // existing state/ref/Effect Event checks at the synchronous render phase.
2691
3151
  if (stateInitializer) currentFunctionChecksImpureCalls = false;
2692
3152
  try {
@@ -2853,26 +3313,92 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2853
3313
  }
2854
3314
  }
2855
3315
 
2856
- function visit(node, scope, phase) {
3316
+ function visit(node, scope, phase, readAccess = true) {
2857
3317
  if (node == null || typeof node !== 'object') return;
2858
3318
  if (Array.isArray(node)) {
2859
3319
  for (const child of node) visit(child, scope, phase);
2860
3320
  return;
2861
3321
  }
2862
3322
  if (TRANSPARENT_EXPRESSIONS.has(node.type)) {
2863
- visit(node.expression, scope, phase);
3323
+ visit(node.expression, scope, phase, readAccess);
2864
3324
  return;
2865
3325
  }
2866
3326
  if (node.type?.startsWith('TS')) return;
2867
3327
 
2868
3328
  switch (node.type) {
2869
3329
  case 'ImportDeclaration':
2870
- case 'Identifier':
3330
+ case 'MetaProperty':
3331
+ case 'BreakStatement':
3332
+ case 'ContinueStatement':
2871
3333
  case 'JSXIdentifier':
2872
3334
  case 'Literal':
2873
3335
  case 'ThisExpression':
2874
3336
  case 'Super':
2875
3337
  return;
3338
+ case 'Identifier':
3339
+ if (
3340
+ readAccess &&
3341
+ phase === 'render' &&
3342
+ currentFunctionChecksImpureCalls &&
3343
+ ambientReference(node, scope)?.kind === 'ambient'
3344
+ ) {
3345
+ reportAmbientRead(node);
3346
+ }
3347
+ if (
3348
+ readAccess &&
3349
+ phase === 'render' &&
3350
+ currentFunctionChecksRenderReads &&
3351
+ mutableModuleRead(node.name, scope)
3352
+ ) {
3353
+ reportModuleStateRead(node);
3354
+ }
3355
+ return;
3356
+ case 'JSXOpeningElement': {
3357
+ let name = node.name;
3358
+ const member = name?.type === 'JSXMemberExpression';
3359
+ let ambientMember;
3360
+ while (name?.type === 'JSXMemberExpression') {
3361
+ if (
3362
+ phase === 'render' &&
3363
+ currentFunctionChecksImpureCalls &&
3364
+ isAmbientMemberRead(name, scope)
3365
+ ) {
3366
+ ambientMember = name;
3367
+ }
3368
+ name = name.object;
3369
+ }
3370
+ if (
3371
+ name?.type === 'JSXIdentifier' &&
3372
+ (member || (!/^[a-z]/.test(name.name) && !name.name.includes('-'))) &&
3373
+ phase === 'render' &&
3374
+ currentFunctionChecksImpureCalls
3375
+ ) {
3376
+ const reference = ambientReference(name, scope);
3377
+ if (reference?.kind === 'ambient' || ambientMember) {
3378
+ reportAmbientRead(ambientMember ?? name);
3379
+ }
3380
+ }
3381
+ if (
3382
+ name?.type === 'JSXIdentifier' &&
3383
+ (member || (!/^[a-z]/.test(name.name) && !name.name.includes('-'))) &&
3384
+ phase === 'render' &&
3385
+ currentFunctionChecksRenderReads &&
3386
+ mutableModuleRead(name.name, scope)
3387
+ ) {
3388
+ reportModuleStateRead(name);
3389
+ }
3390
+ break;
3391
+ }
3392
+ case 'JSXSpreadAttribute':
3393
+ visit(node.argument, scope, phase);
3394
+ if (
3395
+ currentFunctionChecksImpureCalls &&
3396
+ phaseAfter(node.argument, phase) === 'render' &&
3397
+ ambientReference(node.argument, scope) === GLOBAL_OBJECT_BINDING
3398
+ ) {
3399
+ reportAmbientRead(node);
3400
+ }
3401
+ return;
2876
3402
  case 'ExportNamedDeclaration':
2877
3403
  case 'ExportDefaultDeclaration':
2878
3404
  visit(node.declaration, scope, phase);
@@ -2882,6 +3408,25 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2882
3408
  case 'ArrowFunctionExpression':
2883
3409
  visitFunction(node, scope, phase === 'module' ? 'render' : 'deferred');
2884
3410
  return;
3411
+ case 'ClassDeclaration':
3412
+ case 'ClassExpression': {
3413
+ const classScope = createScope(scope, 'block', [], node.id ? [node.id] : []);
3414
+ visit(node.decorators, scope, phase);
3415
+ visit(node.superClass, classScope, phase);
3416
+ visit(node.body, classScope, phase);
3417
+ return;
3418
+ }
3419
+ case 'MethodDefinition':
3420
+ case 'PropertyDefinition':
3421
+ case 'AccessorProperty':
3422
+ visit(node.decorators, scope, phase);
3423
+ if (node.computed) visit(node.key, scope, phase);
3424
+ visit(
3425
+ node.value,
3426
+ scope,
3427
+ node.type === 'MethodDefinition' || node.static ? phase : 'deferred',
3428
+ );
3429
+ return;
2885
3430
  case 'BlockStatement':
2886
3431
  case 'JSXCodeBlock': {
2887
3432
  if (node.type === 'JSXCodeBlock') hasNestedCodeBlock = true;
@@ -2930,7 +3475,22 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2930
3475
  for (const property of node.properties ?? []) {
2931
3476
  if (property.type === 'SpreadElement') {
2932
3477
  visit(property.argument, scope, executionPhase);
2933
- executionPhase = phaseAfter(property.argument, executionPhase);
3478
+ const spreadPhase = phaseAfter(property.argument, executionPhase);
3479
+ if (
3480
+ spreadPhase === 'render' &&
3481
+ currentFunctionChecksImpureCalls &&
3482
+ ambientReference(property.argument, scope) === GLOBAL_OBJECT_BINDING
3483
+ ) {
3484
+ reportAmbientRead(property);
3485
+ }
3486
+ if (
3487
+ spreadPhase === 'render' &&
3488
+ currentFunctionChecksRenderReads &&
3489
+ isRefObject(property.argument, scope)
3490
+ ) {
3491
+ reportRefRead(property);
3492
+ }
3493
+ executionPhase = spreadPhase;
2934
3494
  continue;
2935
3495
  }
2936
3496
  if (property.computed === true) {
@@ -2946,14 +3506,62 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2946
3506
  }
2947
3507
  case 'MemberExpression': {
2948
3508
  visit(node.object, scope, phase);
3509
+ if (
3510
+ node.optional === true
3511
+ ? definitelyNullishOptionalValue(node.object, scope)
3512
+ : definitelySkippedOptionalContinuation(node.object, scope)
3513
+ ) {
3514
+ return;
3515
+ }
2949
3516
  if (node.computed === true) {
2950
- visit(node.property, scope, phaseAfter(node.object, phase, true, node.optional === true));
3517
+ const propertyPhase = phaseAfter(node.object, phase, true, node.optional === true);
3518
+ visit(node.property, scope, propertyPhase);
3519
+ if (
3520
+ readAccess &&
3521
+ propertyPhase === 'render' &&
3522
+ currentFunctionChecksRenderReads &&
3523
+ readCurrentRef(node, scope) &&
3524
+ phaseAfter(node.property, propertyPhase) === 'render'
3525
+ ) {
3526
+ reportRefRead(node);
3527
+ }
3528
+ } else if (
3529
+ readAccess &&
3530
+ phase === 'render' &&
3531
+ currentFunctionChecksRenderReads &&
3532
+ readCurrentRef(node, scope) &&
3533
+ phaseAfter(node.object, phase, true, node.optional === true) === 'render'
3534
+ ) {
3535
+ reportRefRead(node);
3536
+ }
3537
+ if (
3538
+ readAccess &&
3539
+ phase === 'render' &&
3540
+ currentFunctionChecksImpureCalls &&
3541
+ isAmbientMemberRead(node, scope)
3542
+ ) {
3543
+ const propertyPhase = phaseAfter(node.object, phase, true, node.optional === true);
3544
+ if (phaseAfter(node.computed ? node.property : null, propertyPhase) === 'render') {
3545
+ reportAmbientRead(node);
3546
+ }
2951
3547
  }
2952
3548
  return;
2953
3549
  }
2954
3550
  case 'LabeledStatement':
2955
3551
  visit(node.body, scope, phase);
2956
3552
  return;
3553
+ case 'JSXSwitchExpression': {
3554
+ visit(node.discriminant, scope, phase);
3555
+ let searchPhase = phaseAfter(node.discriminant, phase);
3556
+ for (const branch of node.cases ?? []) {
3557
+ visit(branch.test, scope, searchPhase);
3558
+ searchPhase = phaseAfter(branch.test, searchPhase);
3559
+ const caseScope = createScope(scope, 'function', branch.consequent ?? []);
3560
+ predeclareHoistedVars(branch.consequent, caseScope);
3561
+ visitStatements(branch.consequent, caseScope, searchPhase);
3562
+ }
3563
+ return;
3564
+ }
2957
3565
  case 'SwitchStatement': {
2958
3566
  visit(node.discriminant, scope, phase);
2959
3567
  const initialPhase =
@@ -3016,6 +3624,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3016
3624
  if (!FUNCTION_TYPES.has(callee?.type)) {
3017
3625
  visit(node.callee, scope, phase);
3018
3626
  }
3627
+ if (
3628
+ node.optional === true
3629
+ ? definitelyNullishOptionalValue(node.callee, scope)
3630
+ : definitelySkippedOptionalContinuation(node.callee, scope)
3631
+ ) {
3632
+ return;
3633
+ }
3019
3634
  let executionPhase = phaseAfter(node.callee, phase, true, node.optional === true);
3020
3635
  const synchronousCallbackIndex =
3021
3636
  hook === 'useLinkedState'
@@ -3075,11 +3690,14 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3075
3690
  // memo owns a component callback. lazy also accepts a module
3076
3691
  // loader, so require JSX evidence before treating it as a render.
3077
3692
  const checkImpureCalls = currentFunctionChecksImpureCalls;
3693
+ const checkRenderReads = currentFunctionChecksRenderReads;
3078
3694
  currentFunctionChecksImpureCalls = true;
3695
+ currentFunctionChecksRenderReads = true;
3079
3696
  try {
3080
3697
  visitCallback(component.node, component.scope, 'render');
3081
3698
  } finally {
3082
3699
  currentFunctionChecksImpureCalls = checkImpureCalls;
3700
+ currentFunctionChecksRenderReads = checkRenderReads;
3083
3701
  }
3084
3702
  return;
3085
3703
  }
@@ -3110,7 +3728,9 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3110
3728
  callback,
3111
3729
  callee,
3112
3730
  executionPhase,
3113
- callback.kind === 'setter' ? null : argumentValues(node.arguments, scope),
3731
+ callback.kind === 'setter' || callback.kind === 'getter'
3732
+ ? null
3733
+ : argumentValues(node.arguments, scope),
3114
3734
  );
3115
3735
  }
3116
3736
  }
@@ -3185,11 +3805,27 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3185
3805
  case 'AssignmentExpression': {
3186
3806
  if (node.left?.type === 'ArrayPattern' || node.left?.type === 'ObjectPattern') {
3187
3807
  visit(node.right, scope, phase);
3188
- visitPatternExpressions(node.left, scope, phaseAfter(node.right, phase));
3808
+ visitPatternExpressions(
3809
+ node.left,
3810
+ scope,
3811
+ phaseAfter(node.right, phase),
3812
+ node.left.type === 'ObjectPattern' && isRefObject(node.right, scope),
3813
+ node.left.type === 'ObjectPattern' ? ambientReference(node.right, scope) : null,
3814
+ );
3189
3815
  return;
3190
3816
  }
3191
- visit(node.left, scope, phase);
3817
+ visit(node.left, scope, phase, false);
3192
3818
  const rightPhase = phaseAfter(node.left, phase, true);
3819
+ if (
3820
+ node.operator !== '=' &&
3821
+ rightPhase === 'render' &&
3822
+ currentFunctionChecksImpureCalls &&
3823
+ (isAmbientMemberRead(node.left, scope) ||
3824
+ (unwrap(node.left)?.type === 'Identifier' &&
3825
+ ambientReference(node.left, scope)?.kind === 'ambient'))
3826
+ ) {
3827
+ reportAmbientRead(unwrap(node.left));
3828
+ }
3193
3829
  visit(node.right, scope, rightPhase);
3194
3830
  const executionPhase = phaseAfter(node.right, rightPhase);
3195
3831
  if (executionPhase === 'render') {
@@ -3197,18 +3833,55 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3197
3833
  reportSnapshotWrite(node.left, scope);
3198
3834
  reportRetainedRowMutation(node.left, scope);
3199
3835
  }
3836
+ // Compound and logical assignments read the old value before the
3837
+ // right side, even when an await defers the eventual write.
3838
+ if (
3839
+ node.operator !== '=' &&
3840
+ rightPhase === 'render' &&
3841
+ currentFunctionChecksRenderReads &&
3842
+ (executionPhase !== 'render' || !currentRef(node.left, scope)) &&
3843
+ readCurrentRef(node.left, scope)
3844
+ ) {
3845
+ reportRefRead(node.left);
3846
+ }
3847
+ if (
3848
+ node.operator !== '=' &&
3849
+ phase === 'render' &&
3850
+ currentFunctionChecksRenderReads &&
3851
+ unwrap(node.left)?.type === 'Identifier' &&
3852
+ mutableModuleRead(unwrap(node.left).name, scope)
3853
+ ) {
3854
+ reportModuleStateRead(unwrap(node.left));
3855
+ }
3200
3856
  return;
3201
3857
  }
3202
3858
  case 'UpdateExpression':
3859
+ if (
3860
+ phaseAfter(node.argument, phase) === 'render' &&
3861
+ currentFunctionChecksImpureCalls &&
3862
+ (isAmbientMemberRead(node.argument, scope) ||
3863
+ (unwrap(node.argument)?.type === 'Identifier' &&
3864
+ ambientReference(node.argument, scope)?.kind === 'ambient'))
3865
+ ) {
3866
+ reportAmbientRead(unwrap(node.argument));
3867
+ }
3868
+ if (
3869
+ phase === 'render' &&
3870
+ currentFunctionChecksRenderReads &&
3871
+ unwrap(node.argument)?.type === 'Identifier' &&
3872
+ mutableModuleRead(unwrap(node.argument).name, scope)
3873
+ ) {
3874
+ reportModuleStateRead(unwrap(node.argument));
3875
+ }
3203
3876
  if (phase === 'render' && currentRef(node.argument, scope)) reportRef(node.argument);
3204
- visit(node.argument, scope, phase);
3877
+ visit(node.argument, scope, phase, false);
3205
3878
  if (phaseAfter(node.argument, phase, true) === 'render') {
3206
3879
  reportSnapshotWrite(node.argument, scope);
3207
3880
  reportRetainedRowMutation(node.argument, scope);
3208
3881
  }
3209
3882
  return;
3210
3883
  case 'UnaryExpression':
3211
- visit(node.argument, scope, phase);
3884
+ visit(node.argument, scope, phase, node.operator !== 'delete');
3212
3885
  if (node.operator === 'delete' && phaseAfter(node.argument, phase, true) === 'render') {
3213
3886
  reportSnapshotWrite(node.argument, scope);
3214
3887
  reportRetainedRowMutation(node.argument, scope);
@@ -3291,7 +3964,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3291
3964
  (alwaysAwaits(node.right) || (node.type === 'ForOfStatement' && node.await === true))
3292
3965
  ? 'deferred'
3293
3966
  : phase;
3294
- visit(node.left, loop, executionPhase);
3967
+ if (node.left?.type === 'ArrayPattern' || node.left?.type === 'ObjectPattern') {
3968
+ visitPatternExpressions(node.left, loop, executionPhase);
3969
+ } else {
3970
+ visit(node.left, loop, executionPhase, false);
3971
+ }
3295
3972
  visit(node.body, loop, executionPhase);
3296
3973
  return;
3297
3974
  }