octane 0.1.19 → 0.1.21

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.
@@ -0,0 +1,1064 @@
1
+ const STATE_HOOKS = new Set(['useState', 'useReducer', 'useLinkedState']);
2
+ const EFFECT_HOOKS = new Set(['useEffect', 'useLayoutEffect', 'useInsertionEffect']);
3
+ const FUNCTION_TYPES = new Set([
4
+ 'FunctionDeclaration',
5
+ 'FunctionExpression',
6
+ 'ArrowFunctionExpression',
7
+ ]);
8
+ const TRANSPARENT_EXPRESSIONS = new Set([
9
+ 'ChainExpression',
10
+ 'ParenthesizedExpression',
11
+ 'TSAsExpression',
12
+ 'TSInstantiationExpression',
13
+ 'TSNonNullExpression',
14
+ 'TSSatisfiesExpression',
15
+ 'TSTypeAssertion',
16
+ ]);
17
+ const SKIP_KEYS = new Set([
18
+ 'type',
19
+ 'start',
20
+ 'end',
21
+ 'loc',
22
+ 'range',
23
+ 'parent',
24
+ 'metadata',
25
+ 'comments',
26
+ 'tokens',
27
+ 'typeAnnotation',
28
+ 'returnType',
29
+ 'typeParameters',
30
+ ]);
31
+
32
+ export const STRONG_RENDER_STATE_UPDATE = 'OCTANE_STRONG_RENDER_STATE_UPDATE';
33
+ export const STRONG_EFFECT_STATE_UPDATE = 'OCTANE_STRONG_EFFECT_STATE_UPDATE';
34
+ export const STRONG_RENDER_REF_WRITE = 'OCTANE_STRONG_RENDER_REF_WRITE';
35
+ export const STRONG_DIRECTIVE_PLACEMENT = 'OCTANE_STRONG_DIRECTIVE_PLACEMENT';
36
+
37
+ function unwrap(node) {
38
+ let value = node;
39
+ while (value && TRANSPARENT_EXPRESSIONS.has(value.type)) value = value.expression;
40
+ return value;
41
+ }
42
+
43
+ function optionalChainCanSkip(node) {
44
+ let current = node;
45
+ while (current != null) {
46
+ // Grouping ends a chain before an outer call or property access evaluates.
47
+ if (current.type === 'ChainExpression') return false;
48
+ if (TRANSPARENT_EXPRESSIONS.has(current.type)) {
49
+ current = current.expression;
50
+ continue;
51
+ }
52
+ if (current.type !== 'CallExpression' && current.type !== 'MemberExpression') return false;
53
+ if (current.optional === true) return true;
54
+ current = current.type === 'CallExpression' ? current.callee : current.object;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ function addPatternNames(pattern, bindings, value) {
60
+ if (pattern == null) return;
61
+ switch (pattern.type) {
62
+ case 'Identifier':
63
+ bindings.set(pattern.name, value);
64
+ break;
65
+ case 'RestElement':
66
+ addPatternNames(pattern.argument, bindings, value);
67
+ break;
68
+ case 'AssignmentPattern':
69
+ addPatternNames(pattern.left, bindings, value);
70
+ break;
71
+ case 'ArrayPattern':
72
+ for (const element of pattern.elements ?? []) addPatternNames(element, bindings, value);
73
+ break;
74
+ case 'ObjectPattern':
75
+ for (const property of pattern.properties ?? []) {
76
+ addPatternNames(property.argument ?? property.value, bindings, value);
77
+ }
78
+ }
79
+ }
80
+
81
+ function declarationOf(statement) {
82
+ return statement?.type === 'ExportNamedDeclaration' ||
83
+ statement?.type === 'ExportDefaultDeclaration'
84
+ ? statement.declaration
85
+ : statement;
86
+ }
87
+
88
+ function nearestFunctionScope(scope) {
89
+ let current = scope;
90
+ while (current?.parent && current.kind !== 'function') current = current.parent;
91
+ return current;
92
+ }
93
+
94
+ function declarationScope(scope, kind) {
95
+ return kind === 'var' ? nearestFunctionScope(scope) : scope;
96
+ }
97
+
98
+ function predeclareStatements(statements, scope) {
99
+ for (const original of statements ?? []) {
100
+ const node = declarationOf(original);
101
+ if (node?.type === 'ImportDeclaration') {
102
+ const octane = node.source?.value === 'octane' && node.importKind !== 'type';
103
+ for (const specifier of node.specifiers ?? []) {
104
+ if (!specifier.local?.name || specifier.importKind === 'type') continue;
105
+ if (octane && specifier.type === 'ImportNamespaceSpecifier') {
106
+ scope.bindings.set(specifier.local.name, { kind: 'namespace' });
107
+ } else if (octane && specifier.type === 'ImportSpecifier') {
108
+ const hook = specifier.imported?.name ?? specifier.imported?.value;
109
+ scope.bindings.set(specifier.local.name, { kind: 'hook', hook });
110
+ } else {
111
+ scope.bindings.set(specifier.local.name, { kind: 'other' });
112
+ }
113
+ }
114
+ } else if (node?.type === 'VariableDeclaration') {
115
+ const target = declarationScope(scope, node.kind);
116
+ for (const declaration of node.declarations ?? []) {
117
+ addPatternNames(declaration.id, target.bindings, { kind: 'other' });
118
+ }
119
+ } else if (node?.type === 'FunctionDeclaration' && node.id?.name) {
120
+ scope.bindings.set(node.id.name, { kind: 'callback', node, scope });
121
+ } else if (node?.type === 'ClassDeclaration' && node.id?.name) {
122
+ scope.bindings.set(node.id.name, { kind: 'other' });
123
+ }
124
+ }
125
+ }
126
+
127
+ function createScope(parent, kind, statements = [], params = []) {
128
+ const scope = { parent, kind, bindings: new Map() };
129
+ predeclareStatements(statements, scope);
130
+ for (const param of params) addPatternNames(param, scope.bindings, { kind: 'other' });
131
+ return scope;
132
+ }
133
+
134
+ function resolve(scope, name) {
135
+ for (let current = scope; current; current = current.parent) {
136
+ if (current.bindings.has(name)) return current.bindings.get(name);
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function importedHook(callee, scope) {
142
+ const value = unwrap(callee);
143
+ if (value?.type === 'Identifier') {
144
+ const binding = resolve(scope, value.name);
145
+ return binding?.kind === 'hook' ? binding.hook : null;
146
+ }
147
+ if (value?.type !== 'MemberExpression' || value.optional === true) return null;
148
+ const object = unwrap(value.object);
149
+ if (object?.type !== 'Identifier' || resolve(scope, object.name)?.kind !== 'namespace') {
150
+ return null;
151
+ }
152
+ if (!value.computed) {
153
+ return value.property?.type === 'Identifier' ? value.property.name : null;
154
+ }
155
+ const property = unwrap(value.property);
156
+ return property?.type === 'Literal' ? property.value : null;
157
+ }
158
+
159
+ function currentRef(member, scope) {
160
+ const value = unwrap(member);
161
+ if (value?.type !== 'MemberExpression' || value.optional === true) return false;
162
+ const object = unwrap(value.object);
163
+ if (object?.type !== 'Identifier' || resolve(scope, object.name)?.kind !== 'ref') return false;
164
+ if (!value.computed) {
165
+ return value.property?.type === 'Identifier' && value.property.name === 'current';
166
+ }
167
+ const property = unwrap(value.property);
168
+ return property?.type === 'Literal' && property.value === 'current';
169
+ }
170
+
171
+ function sourcePosition(node, boundary) {
172
+ const offset = boundary === 'end' ? (node?.end ?? node?.start ?? 0) : (node?.start ?? 0);
173
+ const position = boundary === 'end' ? (node?.loc?.end ?? node?.loc?.start) : node?.loc?.start;
174
+ return {
175
+ offset,
176
+ line: position?.line ?? 1,
177
+ column: position?.column ?? 0,
178
+ };
179
+ }
180
+
181
+ function diagnostic(code, filename, node, message, suggestions = []) {
182
+ return {
183
+ code,
184
+ severity: 'error',
185
+ filename,
186
+ start: sourcePosition(node, 'start'),
187
+ end: sourcePosition(node, 'end'),
188
+ message,
189
+ suggestions,
190
+ };
191
+ }
192
+
193
+ function strongDirectives(ast) {
194
+ let enabled = false;
195
+ let misplaced = null;
196
+ let prologue = true;
197
+ for (const statement of ast?.body ?? []) {
198
+ if (
199
+ prologue &&
200
+ statement.type === 'ExpressionStatement' &&
201
+ typeof statement.directive === 'string'
202
+ ) {
203
+ if (statement.directive === 'use strong') enabled = true;
204
+ continue;
205
+ }
206
+ prologue = false;
207
+ if (
208
+ misplaced === null &&
209
+ statement.type === 'ExpressionStatement' &&
210
+ statement.expression?.value === 'use strong' &&
211
+ (statement.expression.raw === '"use strong"' || statement.expression.raw === "'use strong'")
212
+ ) {
213
+ misplaced = statement.expression;
214
+ }
215
+ }
216
+ return { enabled, misplaced };
217
+ }
218
+
219
+ /**
220
+ * Analyze only modules that explicitly opted in. The authored parser tree is
221
+ * read-only: scope information stays in local maps instead of annotating AST
222
+ * nodes, so compile, SSR, hydration extraction, and Volar share one contract.
223
+ *
224
+ * @param {any} ast
225
+ * @param {string} source
226
+ * @param {string | undefined} filename
227
+ * @param {{ strong?: boolean }} [options]
228
+ */
229
+ export function analyzeStrongMode(ast, source, filename, options = {}) {
230
+ if (options.strong !== true && !source.includes('use strong')) {
231
+ return { enabled: false, diagnostics: [] };
232
+ }
233
+ const diagnostics = [];
234
+ const directives = strongDirectives(ast);
235
+ if (directives.misplaced !== null) {
236
+ diagnostics.push(
237
+ diagnostic(
238
+ STRONG_DIRECTIVE_PLACEMENT,
239
+ filename,
240
+ directives.misplaced,
241
+ 'Place "use strong" at the top of the file, before imports or other code.',
242
+ ),
243
+ );
244
+ }
245
+ const enabled = options.strong === true || directives.enabled;
246
+ if (!enabled) return { enabled: false, diagnostics };
247
+
248
+ const moduleScope = createScope(null, 'module', ast.body ?? []);
249
+ const activeCallbacks = new Set();
250
+ let currentFunctionIsAsync = false;
251
+
252
+ function reportSetter(node, phase) {
253
+ const effect = phase === 'effect';
254
+ const code = effect ? STRONG_EFFECT_STATE_UPDATE : STRONG_RENDER_STATE_UPDATE;
255
+ const message = effect
256
+ ? 'Strong mode does not allow synchronous state updates inside effect setup. Derive the value during render or use useLinkedState when state follows another value.'
257
+ : 'Strong mode does not allow state updates during render. Use useLinkedState when state needs to reset or change with another value.';
258
+ diagnostics.push(diagnostic(code, filename, node, message, [{ hook: 'useLinkedState' }]));
259
+ }
260
+
261
+ function reportRef(node) {
262
+ diagnostics.push(
263
+ diagnostic(
264
+ STRONG_RENDER_REF_WRITE,
265
+ filename,
266
+ node,
267
+ 'Strong mode does not allow writing to useRef.current during render. Move the write to an event or effect, or express the value as state.',
268
+ ),
269
+ );
270
+ }
271
+
272
+ function alwaysAwaits(expression) {
273
+ const node = unwrap(expression);
274
+ if (node == null) return false;
275
+ switch (node.type) {
276
+ case 'AwaitExpression':
277
+ return true;
278
+ case 'SequenceExpression':
279
+ return (node.expressions ?? []).some(alwaysAwaits);
280
+ case 'CallExpression':
281
+ case 'NewExpression':
282
+ return (
283
+ alwaysAwaits(node.callee) ||
284
+ (node.optional !== true &&
285
+ (node.arguments ?? []).some(alwaysAwaits) &&
286
+ !optionalChainCanSkip(node.callee))
287
+ );
288
+ case 'ClassExpression':
289
+ return alwaysAwaits(node.superClass);
290
+ case 'AssignmentExpression':
291
+ return (
292
+ alwaysAwaits(node.left) ||
293
+ (node.operator !== '&&=' &&
294
+ node.operator !== '||=' &&
295
+ node.operator !== '??=' &&
296
+ alwaysAwaits(node.right))
297
+ );
298
+ case 'BinaryExpression':
299
+ return alwaysAwaits(node.left) || alwaysAwaits(node.right);
300
+ case 'LogicalExpression':
301
+ return alwaysAwaits(node.left);
302
+ case 'ConditionalExpression':
303
+ return (
304
+ alwaysAwaits(node.test) || (alwaysAwaits(node.consequent) && alwaysAwaits(node.alternate))
305
+ );
306
+ case 'UnaryExpression':
307
+ case 'UpdateExpression':
308
+ return alwaysAwaits(node.argument);
309
+ case 'MemberExpression':
310
+ return (
311
+ alwaysAwaits(node.object) ||
312
+ (node.optional !== true &&
313
+ node.computed === true &&
314
+ alwaysAwaits(node.property) &&
315
+ !optionalChainCanSkip(node.object))
316
+ );
317
+ case 'ArrayExpression':
318
+ return (node.elements ?? []).some(alwaysAwaits);
319
+ case 'ObjectExpression':
320
+ return (node.properties ?? []).some((property) =>
321
+ property.type === 'SpreadElement'
322
+ ? alwaysAwaits(property.argument)
323
+ : (property.computed === true && alwaysAwaits(property.key)) ||
324
+ (property.kind === 'init' &&
325
+ property.method !== true &&
326
+ alwaysAwaits(property.value)),
327
+ );
328
+ case 'SpreadElement':
329
+ return alwaysAwaits(node.argument);
330
+ case 'TemplateLiteral':
331
+ return (node.expressions ?? []).some(alwaysAwaits);
332
+ case 'TaggedTemplateExpression':
333
+ return alwaysAwaits(node.tag) || alwaysAwaits(node.quasi);
334
+ default:
335
+ return false;
336
+ }
337
+ }
338
+
339
+ function patternAlwaysAwaits(pattern) {
340
+ switch (pattern?.type) {
341
+ case 'ArrayPattern':
342
+ return (pattern.elements ?? []).some(patternAlwaysAwaits);
343
+ case 'ObjectPattern':
344
+ return (pattern.properties ?? []).some(
345
+ (property) =>
346
+ (property.computed === true && alwaysAwaits(property.key)) ||
347
+ patternAlwaysAwaits(property.argument ?? property.value),
348
+ );
349
+ case 'AssignmentPattern':
350
+ return patternAlwaysAwaits(pattern.left);
351
+ case 'RestElement':
352
+ return patternAlwaysAwaits(pattern.argument);
353
+ default:
354
+ return false;
355
+ }
356
+ }
357
+
358
+ // Bit 1 exits this loop before an await; bit 2 reaches its test synchronously.
359
+ function doWhileSynchronousControl(statement, nestedBreaks = 0, nestedLoops = 0, labels = null) {
360
+ switch (statement?.type) {
361
+ case 'BreakStatement': {
362
+ if (statement.label == null) return nestedBreaks === 0 ? 1 : 0;
363
+ for (let label = labels; label; label = label.parent) {
364
+ if (label.name === statement.label.name) return 0;
365
+ }
366
+ return 1;
367
+ }
368
+ case 'ContinueStatement': {
369
+ if (statement.label == null) return nestedLoops === 0 ? 2 : 0;
370
+ for (let label = labels; label; label = label.parent) {
371
+ if (label.name === statement.label.name) return 0;
372
+ }
373
+ return 2;
374
+ }
375
+ case 'BlockStatement':
376
+ case 'JSXCodeBlock': {
377
+ let control = 0;
378
+ for (const child of statement.body ?? []) {
379
+ control |= doWhileSynchronousControl(child, nestedBreaks, nestedLoops, labels);
380
+ if (
381
+ statementAlwaysAwaits(child) ||
382
+ child.type === 'BreakStatement' ||
383
+ child.type === 'ContinueStatement' ||
384
+ child.type === 'ReturnStatement' ||
385
+ child.type === 'ThrowStatement'
386
+ ) {
387
+ return control;
388
+ }
389
+ }
390
+ return control;
391
+ }
392
+ case 'IfStatement':
393
+ return alwaysAwaits(statement.test)
394
+ ? 0
395
+ : doWhileSynchronousControl(statement.consequent, nestedBreaks, nestedLoops, labels) |
396
+ doWhileSynchronousControl(statement.alternate, nestedBreaks, nestedLoops, labels);
397
+ case 'LabeledStatement':
398
+ return doWhileSynchronousControl(statement.body, nestedBreaks, nestedLoops, {
399
+ name: statement.label.name,
400
+ parent: labels,
401
+ });
402
+ case 'SwitchStatement': {
403
+ if (alwaysAwaits(statement.discriminant)) return 0;
404
+ let control = 0;
405
+ for (const branch of statement.cases ?? []) {
406
+ for (const child of branch.consequent ?? []) {
407
+ control |= doWhileSynchronousControl(child, nestedBreaks + 1, nestedLoops, labels);
408
+ if (statementAlwaysAwaits(child) || child.type === 'BreakStatement') break;
409
+ }
410
+ }
411
+ return control;
412
+ }
413
+ case 'ForStatement':
414
+ case 'ForInStatement':
415
+ case 'ForOfStatement':
416
+ case 'WhileStatement':
417
+ case 'DoWhileStatement':
418
+ return statementAlwaysAwaits(statement)
419
+ ? 0
420
+ : doWhileSynchronousControl(statement.body, nestedBreaks + 1, nestedLoops + 1, labels);
421
+ case 'TryStatement':
422
+ if (statement.finalizer != null && statementAlwaysAwaits(statement.finalizer)) return 0;
423
+ return (
424
+ doWhileSynchronousControl(statement.block, nestedBreaks, nestedLoops, labels) |
425
+ doWhileSynchronousControl(statement.handler?.body, nestedBreaks, nestedLoops, labels) |
426
+ doWhileSynchronousControl(statement.finalizer, nestedBreaks, nestedLoops, labels)
427
+ );
428
+ default:
429
+ return 0;
430
+ }
431
+ }
432
+
433
+ function switchCaseAlwaysAwaits(cases, start) {
434
+ for (let index = start; index < cases.length; index++) {
435
+ for (const statement of cases[index].consequent ?? []) {
436
+ if (doWhileSynchronousControl(statement) !== 0) return false;
437
+ if (statementAlwaysAwaits(statement)) return true;
438
+ if (
439
+ statement.type === 'BreakStatement' ||
440
+ statement.type === 'ContinueStatement' ||
441
+ statement.type === 'ReturnStatement' ||
442
+ statement.type === 'ThrowStatement'
443
+ ) {
444
+ return false;
445
+ }
446
+ }
447
+ }
448
+ return false;
449
+ }
450
+
451
+ function statementAlwaysAwaits(statement) {
452
+ switch (statement?.type) {
453
+ case 'ExpressionStatement':
454
+ return alwaysAwaits(statement.expression);
455
+ case 'VariableDeclaration':
456
+ return (statement.declarations ?? []).some(
457
+ (declaration) => alwaysAwaits(declaration.init) || patternAlwaysAwaits(declaration.id),
458
+ );
459
+ case 'ClassDeclaration':
460
+ return alwaysAwaits(statement.superClass);
461
+ case 'ReturnStatement':
462
+ case 'ThrowStatement':
463
+ return alwaysAwaits(statement.argument);
464
+ case 'BlockStatement':
465
+ case 'JSXCodeBlock':
466
+ for (const child of statement.body ?? []) {
467
+ if (statementAlwaysAwaits(child)) return true;
468
+ }
469
+ return false;
470
+ case 'IfStatement':
471
+ return (
472
+ alwaysAwaits(statement.test) ||
473
+ (statement.alternate != null &&
474
+ statementAlwaysAwaits(statement.consequent) &&
475
+ statementAlwaysAwaits(statement.alternate))
476
+ );
477
+ case 'ForStatement':
478
+ return (
479
+ (statement.init?.type === 'VariableDeclaration'
480
+ ? statementAlwaysAwaits(statement.init)
481
+ : alwaysAwaits(statement.init)) || alwaysAwaits(statement.test)
482
+ );
483
+ case 'ForInStatement':
484
+ return alwaysAwaits(statement.right);
485
+ case 'ForOfStatement':
486
+ return statement.await === true || alwaysAwaits(statement.right);
487
+ case 'WhileStatement':
488
+ return alwaysAwaits(statement.test);
489
+ case 'LabeledStatement':
490
+ return (
491
+ statementAlwaysAwaits(statement.body) &&
492
+ (statement.body?.type === 'DoWhileStatement' ||
493
+ doWhileSynchronousControl(statement.body) === 0)
494
+ );
495
+ case 'SwitchStatement': {
496
+ if (alwaysAwaits(statement.discriminant)) return true;
497
+ const cases = statement.cases ?? [];
498
+ if (!cases.some((branch) => branch.test == null)) {
499
+ return alwaysAwaits(cases[0]?.test);
500
+ }
501
+ let searchAwaits = false;
502
+ const fallbackAwaits = cases.some((branch) => alwaysAwaits(branch.test));
503
+ for (let index = 0; index < cases.length; index++) {
504
+ const branch = cases[index];
505
+ if (branch.test != null && alwaysAwaits(branch.test)) searchAwaits = true;
506
+ if (
507
+ !(branch.test == null ? fallbackAwaits : searchAwaits) &&
508
+ !switchCaseAlwaysAwaits(cases, index)
509
+ ) {
510
+ return false;
511
+ }
512
+ }
513
+ return true;
514
+ }
515
+ case 'DoWhileStatement': {
516
+ const control = doWhileSynchronousControl(statement.body);
517
+ const bodyAwaits = statementAlwaysAwaits(statement.body) && control === 0;
518
+ return (control & 1) === 0 && (bodyAwaits || alwaysAwaits(statement.test));
519
+ }
520
+ case 'TryStatement':
521
+ return (
522
+ (statement.finalizer != null && statementAlwaysAwaits(statement.finalizer)) ||
523
+ (statementAlwaysAwaits(statement.block) &&
524
+ (statement.handler == null || statementAlwaysAwaits(statement.handler.body)))
525
+ );
526
+ default:
527
+ return false;
528
+ }
529
+ }
530
+
531
+ function continuedChainAlwaysAwaits(expression, groupedOptional = false) {
532
+ if (expression?.type === 'ChainExpression') {
533
+ return groupedOptional
534
+ ? continuedChainAlwaysAwaits(expression.expression)
535
+ : alwaysAwaits(expression);
536
+ }
537
+ const node = unwrap(expression);
538
+ if (node == null) return false;
539
+ if (node.type === 'MemberExpression') {
540
+ return (
541
+ continuedChainAlwaysAwaits(node.object) ||
542
+ (node.computed === true && alwaysAwaits(node.property))
543
+ );
544
+ }
545
+ if (node.type === 'CallExpression') {
546
+ return continuedChainAlwaysAwaits(node.callee) || (node.arguments ?? []).some(alwaysAwaits);
547
+ }
548
+ return alwaysAwaits(node);
549
+ }
550
+
551
+ function phaseAfter(expression, phase, continued = false, groupedOptional = false) {
552
+ if (!currentFunctionIsAsync || phase === 'deferred') return phase;
553
+ if (
554
+ alwaysAwaits(expression) ||
555
+ (continued &&
556
+ ((optionalChainCanSkip(expression) && continuedChainAlwaysAwaits(expression)) ||
557
+ (groupedOptional && continuedChainAlwaysAwaits(expression, true))))
558
+ ) {
559
+ return 'deferred';
560
+ }
561
+ return phase;
562
+ }
563
+
564
+ function visitExpressionList(expressions, scope, phase) {
565
+ let executionPhase = phase;
566
+ for (const expression of expressions ?? []) {
567
+ visit(expression, scope, executionPhase);
568
+ executionPhase = phaseAfter(expression, executionPhase);
569
+ }
570
+ return executionPhase;
571
+ }
572
+
573
+ function visitStatements(statements, scope, phase) {
574
+ let executionPhase = phase;
575
+ for (const statement of statements ?? []) {
576
+ visit(statement, scope, executionPhase);
577
+ if (
578
+ currentFunctionIsAsync &&
579
+ executionPhase !== 'deferred' &&
580
+ statementAlwaysAwaits(statement)
581
+ ) {
582
+ executionPhase = 'deferred';
583
+ }
584
+ }
585
+ return executionPhase;
586
+ }
587
+
588
+ function visitFunction(node, parentScope, phase) {
589
+ const enclosingFunctionIsAsync = currentFunctionIsAsync;
590
+ currentFunctionIsAsync = node.async === true;
591
+ try {
592
+ const body = node.body;
593
+ const statements =
594
+ body?.type === 'BlockStatement' || body?.type === 'JSXCodeBlock' ? body.body : [];
595
+ const functionScope = createScope(parentScope, 'function', statements, node.params ?? []);
596
+ if (node.id?.name) functionScope.bindings.set(node.id.name, { kind: 'other' });
597
+ for (const parameter of node.params ?? []) {
598
+ visitPatternExpressions(parameter, functionScope, phase);
599
+ }
600
+ if (body?.type === 'BlockStatement' || body?.type === 'JSXCodeBlock') {
601
+ const executionPhase = visitStatements(body.body, functionScope, phase);
602
+ if (body.render) visit(body.render, functionScope, executionPhase);
603
+ } else {
604
+ visit(body, functionScope, phase);
605
+ }
606
+ } finally {
607
+ currentFunctionIsAsync = enclosingFunctionIsAsync;
608
+ }
609
+ }
610
+
611
+ function visitPatternExpressions(pattern, scope, phase) {
612
+ let executionPhase = phase;
613
+ if (pattern?.type === 'AssignmentPattern') {
614
+ executionPhase = visitPatternExpressions(pattern.left, scope, executionPhase);
615
+ visit(pattern.right, scope, executionPhase);
616
+ } else if (pattern?.type === 'ArrayPattern') {
617
+ for (const element of pattern.elements ?? []) {
618
+ executionPhase = visitPatternExpressions(element, scope, executionPhase);
619
+ }
620
+ } else if (pattern?.type === 'ObjectPattern') {
621
+ for (const property of pattern.properties ?? []) {
622
+ if (property.computed) {
623
+ visit(property.key, scope, executionPhase);
624
+ executionPhase = phaseAfter(property.key, executionPhase);
625
+ }
626
+ executionPhase = visitPatternExpressions(
627
+ property.argument ?? property.value,
628
+ scope,
629
+ executionPhase,
630
+ );
631
+ }
632
+ } else if (pattern?.type === 'RestElement') {
633
+ executionPhase = visitPatternExpressions(pattern.argument, scope, executionPhase);
634
+ }
635
+ return executionPhase;
636
+ }
637
+
638
+ function bindDeclaration(declaration, declarationKind, scope, phase) {
639
+ const target = declarationScope(scope, declarationKind);
640
+ const initial = unwrap(declaration.init);
641
+ const stateTuple =
642
+ (initial?.type === 'CallExpression' &&
643
+ STATE_HOOKS.has(importedHook(initial.callee, scope))) ||
644
+ (initial?.type === 'Identifier' && resolve(scope, initial.name)?.kind === 'state-tuple');
645
+ if (declaration.id?.type === 'ArrayPattern' && stateTuple) {
646
+ const element = declaration.id.elements?.[1];
647
+ const setter = element?.type === 'AssignmentPattern' ? element.left : element;
648
+ if (setter?.type === 'Identifier') target.bindings.set(setter.name, { kind: 'setter' });
649
+ } else if (declaration.id?.type === 'Identifier') {
650
+ if (stateTuple) {
651
+ target.bindings.set(declaration.id.name, { kind: 'state-tuple' });
652
+ } else if (
653
+ initial?.type === 'CallExpression' &&
654
+ importedHook(initial.callee, scope) === 'useRef'
655
+ ) {
656
+ target.bindings.set(declaration.id.name, { kind: 'ref' });
657
+ } else if (declarationKind === 'const' && initial?.type === 'Identifier') {
658
+ const value = resolve(scope, initial.name);
659
+ if (value?.kind === 'setter' || value?.kind === 'ref' || value?.kind === 'callback') {
660
+ target.bindings.set(declaration.id.name, value);
661
+ }
662
+ } else if (declarationKind === 'const' && FUNCTION_TYPES.has(initial?.type)) {
663
+ target.bindings.set(declaration.id.name, { kind: 'callback', node: initial, scope });
664
+ }
665
+ }
666
+ visit(declaration.init, scope, phase);
667
+ return visitPatternExpressions(declaration.id, scope, phaseAfter(declaration.init, phase));
668
+ }
669
+
670
+ function visitCallback(node, parentScope, phase) {
671
+ if (activeCallbacks.has(node)) return;
672
+ activeCallbacks.add(node);
673
+ try {
674
+ visitFunction(node, parentScope, phase);
675
+ } finally {
676
+ activeCallbacks.delete(node);
677
+ }
678
+ }
679
+
680
+ function stateTupleUpdater(value, scope) {
681
+ const member = unwrap(value);
682
+ if (member?.type !== 'MemberExpression' || member.computed !== true) return false;
683
+ const property = unwrap(member.property);
684
+ return (
685
+ property?.type === 'Literal' &&
686
+ (property.value === 1 || property.value === '1') &&
687
+ unwrap(member.object)?.type === 'Identifier' &&
688
+ resolve(scope, unwrap(member.object).name)?.kind === 'state-tuple'
689
+ );
690
+ }
691
+
692
+ function visitSynchronousHookCallback(value, scope, phase) {
693
+ const callback = unwrap(value);
694
+ if (FUNCTION_TYPES.has(callback?.type)) {
695
+ visitCallback(callback, scope, phase);
696
+ return true;
697
+ } else if (callback?.type === 'Identifier') {
698
+ const binding = resolve(scope, callback.name);
699
+ if (binding?.kind === 'callback') visitCallback(binding.node, binding.scope, phase);
700
+ else if (binding?.kind === 'setter' && (phase === 'render' || phase === 'effect')) {
701
+ reportSetter(callback, phase);
702
+ }
703
+ return true;
704
+ } else if (stateTupleUpdater(callback, scope)) {
705
+ if (phase === 'render' || phase === 'effect') reportSetter(callback, phase);
706
+ return true;
707
+ }
708
+ return false;
709
+ }
710
+
711
+ function visit(node, scope, phase) {
712
+ if (node == null || typeof node !== 'object') return;
713
+ if (Array.isArray(node)) {
714
+ for (const child of node) visit(child, scope, phase);
715
+ return;
716
+ }
717
+ if (TRANSPARENT_EXPRESSIONS.has(node.type)) {
718
+ visit(node.expression, scope, phase);
719
+ return;
720
+ }
721
+ if (node.type?.startsWith('TS')) return;
722
+
723
+ switch (node.type) {
724
+ case 'ImportDeclaration':
725
+ case 'Identifier':
726
+ case 'JSXIdentifier':
727
+ case 'Literal':
728
+ case 'ThisExpression':
729
+ case 'Super':
730
+ return;
731
+ case 'ExportNamedDeclaration':
732
+ case 'ExportDefaultDeclaration':
733
+ visit(node.declaration, scope, phase);
734
+ return;
735
+ case 'FunctionDeclaration':
736
+ case 'FunctionExpression':
737
+ case 'ArrowFunctionExpression':
738
+ visitFunction(node, scope, phase === 'module' ? 'render' : 'deferred');
739
+ return;
740
+ case 'BlockStatement':
741
+ case 'JSXCodeBlock': {
742
+ const block = createScope(scope, 'block', node.body ?? []);
743
+ const executionPhase = visitStatements(node.body, block, phase);
744
+ if (node.render) visit(node.render, block, executionPhase);
745
+ return;
746
+ }
747
+ case 'IfStatement': {
748
+ visit(node.test, scope, phase);
749
+ const branchPhase = phaseAfter(node.test, phase);
750
+ visit(node.consequent, scope, branchPhase);
751
+ visit(node.alternate, scope, branchPhase);
752
+ return;
753
+ }
754
+ case 'ConditionalExpression': {
755
+ visit(node.test, scope, phase);
756
+ const branchPhase = phaseAfter(node.test, phase);
757
+ visit(node.consequent, scope, branchPhase);
758
+ visit(node.alternate, scope, branchPhase);
759
+ return;
760
+ }
761
+ case 'LogicalExpression':
762
+ case 'BinaryExpression':
763
+ visit(node.left, scope, phase);
764
+ visit(node.right, scope, phaseAfter(node.left, phase));
765
+ return;
766
+ case 'SequenceExpression':
767
+ visitExpressionList(node.expressions, scope, phase);
768
+ return;
769
+ case 'ArrayExpression':
770
+ visitExpressionList(node.elements, scope, phase);
771
+ return;
772
+ case 'TemplateLiteral':
773
+ visitExpressionList(node.expressions, scope, phase);
774
+ return;
775
+ case 'ObjectExpression': {
776
+ let executionPhase = phase;
777
+ for (const property of node.properties ?? []) {
778
+ if (property.type === 'SpreadElement') {
779
+ visit(property.argument, scope, executionPhase);
780
+ executionPhase = phaseAfter(property.argument, executionPhase);
781
+ continue;
782
+ }
783
+ if (property.computed === true) {
784
+ visit(property.key, scope, executionPhase);
785
+ executionPhase = phaseAfter(property.key, executionPhase);
786
+ }
787
+ if (property.kind === 'init' && property.method !== true) {
788
+ visit(property.value, scope, executionPhase);
789
+ executionPhase = phaseAfter(property.value, executionPhase);
790
+ }
791
+ }
792
+ return;
793
+ }
794
+ case 'MemberExpression': {
795
+ visit(node.object, scope, phase);
796
+ if (node.computed === true) {
797
+ visit(node.property, scope, phaseAfter(node.object, phase, true, node.optional === true));
798
+ }
799
+ return;
800
+ }
801
+ case 'LabeledStatement':
802
+ visit(node.body, scope, phase);
803
+ return;
804
+ case 'SwitchStatement': {
805
+ visit(node.discriminant, scope, phase);
806
+ const initialPhase =
807
+ currentFunctionIsAsync && phase !== 'deferred' && alwaysAwaits(node.discriminant)
808
+ ? 'deferred'
809
+ : phase;
810
+ const branches = node.cases ?? [];
811
+ const switchScope = createScope(scope, 'block');
812
+ for (const branch of branches) predeclareStatements(branch.consequent, switchScope);
813
+
814
+ let searchPhase = initialPhase;
815
+ for (const branch of branches) {
816
+ visit(branch.test, switchScope, searchPhase);
817
+ if (currentFunctionIsAsync && searchPhase !== 'deferred' && alwaysAwaits(branch.test)) {
818
+ searchPhase = 'deferred';
819
+ }
820
+ }
821
+
822
+ const fallbackPhase = searchPhase;
823
+ searchPhase = initialPhase;
824
+ let fallthroughPhase = null;
825
+ for (const branch of branches) {
826
+ if (currentFunctionIsAsync && searchPhase !== 'deferred' && alwaysAwaits(branch.test)) {
827
+ searchPhase = 'deferred';
828
+ }
829
+ const matchingPhase = branch.test == null ? fallbackPhase : searchPhase;
830
+ const branchPhase =
831
+ matchingPhase === 'deferred' &&
832
+ (fallthroughPhase == null || fallthroughPhase === 'deferred')
833
+ ? 'deferred'
834
+ : phase;
835
+ const executionPhase = visitStatements(branch.consequent, switchScope, branchPhase);
836
+ const last = branch.consequent?.[branch.consequent.length - 1];
837
+ fallthroughPhase =
838
+ last?.type === 'BreakStatement' ||
839
+ last?.type === 'ContinueStatement' ||
840
+ last?.type === 'ReturnStatement' ||
841
+ last?.type === 'ThrowStatement'
842
+ ? null
843
+ : executionPhase;
844
+ }
845
+ return;
846
+ }
847
+ case 'VariableDeclaration': {
848
+ let declarationPhase = phase;
849
+ for (const declaration of node.declarations ?? []) {
850
+ declarationPhase = bindDeclaration(declaration, node.kind, scope, declarationPhase);
851
+ }
852
+ return;
853
+ }
854
+ case 'CallExpression': {
855
+ const hook = importedHook(node.callee, scope);
856
+ const callee = unwrap(node.callee);
857
+ const calleeBinding = callee?.type === 'Identifier' ? resolve(scope, callee.name) : null;
858
+ const tupleUpdater = stateTupleUpdater(callee, scope);
859
+ if (!FUNCTION_TYPES.has(callee?.type)) {
860
+ visit(node.callee, scope, phase);
861
+ }
862
+ let executionPhase = phaseAfter(node.callee, phase, true, node.optional === true);
863
+ const synchronousHook = EFFECT_HOOKS.has(hook) || hook === 'useMemo';
864
+ for (let index = 0; index < (node.arguments?.length ?? 0); index++) {
865
+ const argument = node.arguments[index];
866
+ if (index !== 0 || !synchronousHook || !FUNCTION_TYPES.has(unwrap(argument)?.type)) {
867
+ visit(argument, scope, executionPhase);
868
+ }
869
+ executionPhase = phaseAfter(argument, executionPhase);
870
+ }
871
+ if (
872
+ (executionPhase === 'render' || executionPhase === 'effect') &&
873
+ (calleeBinding?.kind === 'setter' || tupleUpdater)
874
+ ) {
875
+ reportSetter(callee, executionPhase);
876
+ }
877
+ if (EFFECT_HOOKS.has(hook)) {
878
+ visitSynchronousHookCallback(node.arguments?.[0], scope, 'effect');
879
+ return;
880
+ }
881
+ if (hook === 'useMemo') {
882
+ visitSynchronousHookCallback(node.arguments?.[0], scope, executionPhase);
883
+ return;
884
+ }
885
+ if (FUNCTION_TYPES.has(callee?.type)) {
886
+ visitCallback(callee, scope, executionPhase);
887
+ } else if (
888
+ (executionPhase === 'render' || executionPhase === 'effect') &&
889
+ calleeBinding?.kind === 'callback'
890
+ ) {
891
+ visitCallback(calleeBinding.node, calleeBinding.scope, executionPhase);
892
+ }
893
+ return;
894
+ }
895
+ case 'NewExpression': {
896
+ const callee = unwrap(node.callee);
897
+ const binding = callee?.type === 'Identifier' ? resolve(scope, callee.name) : null;
898
+ const inlineConstructor =
899
+ (callee?.type === 'FunctionExpression' || callee?.type === 'FunctionDeclaration') &&
900
+ callee.async !== true &&
901
+ callee.generator !== true;
902
+ if (!FUNCTION_TYPES.has(callee?.type)) visit(node.callee, scope, phase);
903
+ const executionPhase = visitExpressionList(
904
+ node.arguments,
905
+ scope,
906
+ phaseAfter(node.callee, phase),
907
+ );
908
+ if (inlineConstructor) {
909
+ visitCallback(callee, scope, executionPhase);
910
+ } else if (
911
+ (executionPhase === 'render' || executionPhase === 'effect') &&
912
+ binding?.kind === 'callback' &&
913
+ (binding.node.type === 'FunctionExpression' ||
914
+ binding.node.type === 'FunctionDeclaration') &&
915
+ binding.node.async !== true &&
916
+ binding.node.generator !== true
917
+ ) {
918
+ visitCallback(binding.node, binding.scope, executionPhase);
919
+ }
920
+ return;
921
+ }
922
+ case 'TaggedTemplateExpression': {
923
+ const tag = unwrap(node.tag);
924
+ const binding = tag?.type === 'Identifier' ? resolve(scope, tag.name) : null;
925
+ if (!FUNCTION_TYPES.has(tag?.type)) visit(node.tag, scope, phase);
926
+ const tagPhase = phaseAfter(node.tag, phase, true);
927
+ visit(node.quasi, scope, tagPhase);
928
+ const executionPhase = phaseAfter(node.quasi, tagPhase);
929
+ if (
930
+ (executionPhase === 'render' || executionPhase === 'effect') &&
931
+ binding?.kind === 'setter'
932
+ ) {
933
+ reportSetter(tag, executionPhase);
934
+ } else if (FUNCTION_TYPES.has(tag?.type)) {
935
+ visitCallback(tag, scope, executionPhase);
936
+ } else if (
937
+ (executionPhase === 'render' || executionPhase === 'effect') &&
938
+ binding?.kind === 'callback'
939
+ ) {
940
+ visitCallback(binding.node, binding.scope, executionPhase);
941
+ }
942
+ return;
943
+ }
944
+ case 'AssignmentExpression': {
945
+ if (node.left?.type === 'ArrayPattern' || node.left?.type === 'ObjectPattern') {
946
+ visit(node.right, scope, phase);
947
+ visitPatternExpressions(node.left, scope, phaseAfter(node.right, phase));
948
+ return;
949
+ }
950
+ visit(node.left, scope, phase);
951
+ const rightPhase = phaseAfter(node.left, phase, true);
952
+ visit(node.right, scope, rightPhase);
953
+ const executionPhase = phaseAfter(node.right, rightPhase);
954
+ if (executionPhase === 'render' && currentRef(node.left, scope)) reportRef(node.left);
955
+ return;
956
+ }
957
+ case 'UpdateExpression':
958
+ if (phase === 'render' && currentRef(node.argument, scope)) reportRef(node.argument);
959
+ visit(node.argument, scope, phase);
960
+ return;
961
+ case 'CatchClause': {
962
+ const catchScope = createScope(scope, 'block', [], node.param ? [node.param] : []);
963
+ visit(node.body, catchScope, phase);
964
+ return;
965
+ }
966
+ case 'ForStatement': {
967
+ const loop = createScope(scope, 'block');
968
+ if (node.init?.type === 'VariableDeclaration') {
969
+ const target = declarationScope(loop, node.init.kind);
970
+ const binding = { kind: 'other' };
971
+ for (const declaration of node.init.declarations ?? []) {
972
+ addPatternNames(declaration.id, target.bindings, binding);
973
+ }
974
+ }
975
+ let executionPhase = phase;
976
+ visit(node.init, loop, executionPhase);
977
+ if (
978
+ currentFunctionIsAsync &&
979
+ executionPhase !== 'deferred' &&
980
+ (node.init?.type === 'VariableDeclaration'
981
+ ? statementAlwaysAwaits(node.init)
982
+ : alwaysAwaits(node.init))
983
+ ) {
984
+ executionPhase = 'deferred';
985
+ }
986
+ visit(node.test, loop, executionPhase);
987
+ if (currentFunctionIsAsync && executionPhase !== 'deferred' && alwaysAwaits(node.test)) {
988
+ executionPhase = 'deferred';
989
+ }
990
+ visit(node.body, loop, executionPhase);
991
+ visit(node.update, loop, executionPhase);
992
+ return;
993
+ }
994
+ case 'ForInStatement':
995
+ case 'ForOfStatement': {
996
+ const loop = createScope(scope, 'block');
997
+ if (node.left?.type === 'VariableDeclaration') {
998
+ const target = declarationScope(loop, node.left.kind);
999
+ const binding = { kind: 'other' };
1000
+ for (const declaration of node.left.declarations ?? []) {
1001
+ addPatternNames(declaration.id, target.bindings, binding);
1002
+ }
1003
+ }
1004
+ visit(node.right, loop, phase);
1005
+ const executionPhase =
1006
+ currentFunctionIsAsync &&
1007
+ phase !== 'deferred' &&
1008
+ (alwaysAwaits(node.right) || (node.type === 'ForOfStatement' && node.await === true))
1009
+ ? 'deferred'
1010
+ : phase;
1011
+ visit(node.left, loop, executionPhase);
1012
+ visit(node.body, loop, executionPhase);
1013
+ return;
1014
+ }
1015
+ case 'WhileStatement': {
1016
+ visit(node.test, scope, phase);
1017
+ const executionPhase =
1018
+ currentFunctionIsAsync && phase !== 'deferred' && alwaysAwaits(node.test)
1019
+ ? 'deferred'
1020
+ : phase;
1021
+ visit(node.body, scope, executionPhase);
1022
+ return;
1023
+ }
1024
+ case 'DoWhileStatement': {
1025
+ visit(node.body, scope, phase);
1026
+ const executionPhase =
1027
+ currentFunctionIsAsync &&
1028
+ phase !== 'deferred' &&
1029
+ statementAlwaysAwaits(node.body) &&
1030
+ (doWhileSynchronousControl(node.body) & 2) === 0
1031
+ ? 'deferred'
1032
+ : phase;
1033
+ visit(node.test, scope, executionPhase);
1034
+ return;
1035
+ }
1036
+ }
1037
+
1038
+ for (const key in node) {
1039
+ if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) visit(node[key], scope, phase);
1040
+ }
1041
+ }
1042
+
1043
+ for (const statement of ast.body ?? []) visit(statement, moduleScope, 'module');
1044
+ return { enabled, diagnostics };
1045
+ }
1046
+
1047
+ /** Throw the first Strong-mode violation using the original authored location. */
1048
+ export function assertStrongMode(ast, source, filename, options) {
1049
+ if (options?.strong !== true && !source.includes('use strong')) return;
1050
+ const result = analyzeStrongMode(ast, source, filename, options);
1051
+ const violation = result.diagnostics[0];
1052
+ if (violation === undefined) return result;
1053
+ const { code, message } = violation;
1054
+ const { line, column } = violation.start;
1055
+ const error = new SyntaxError(
1056
+ `${filename ?? '<anonymous>'}:${line}:${column + 1}: [${code}] ${message}`,
1057
+ );
1058
+ error.code = code;
1059
+ error.filename = filename;
1060
+ error.loc = { line, column };
1061
+ error.pos = violation.start.offset;
1062
+ error.end = violation.end.offset;
1063
+ throw error;
1064
+ }