octane 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/css.cjs +9 -0
- package/dist/cjs/dom-tables.cjs +6 -1
- package/dist/cjs/runtime.cjs +122 -27
- package/dist/cjs/runtime.server.cjs +258 -63
- package/dist/cjs/server/index.cjs +4 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/compiler/bundler.js +7 -0
- package/dist/compiler/compile.js +258 -528
- package/dist/compiler/hydrate-boundaries.js +165 -15
- package/dist/compiler/parser.node.js +34 -8
- package/dist/compiler/strong-mode.js +836 -3
- package/dist/compiler/style-scopes.js +1134 -0
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.js +4543 -2839
- package/dist/css.d.ts +7 -0
- package/dist/css.js +8 -0
- package/dist/dom-tables.js +6 -1
- package/dist/runtime.d.ts +19 -3
- package/dist/runtime.js +129 -28
- package/dist/runtime.server.d.ts +28 -1
- package/dist/runtime.server.js +263 -64
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +4 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -13,6 +13,21 @@ const ARRAY_MUTATORS = new Set([
|
|
|
13
13
|
'splice',
|
|
14
14
|
'unshift',
|
|
15
15
|
]);
|
|
16
|
+
const LOCAL_VALUE_HOOKS = new Set([
|
|
17
|
+
...STATE_HOOKS,
|
|
18
|
+
'useMemo',
|
|
19
|
+
'useCallback',
|
|
20
|
+
'useRef',
|
|
21
|
+
'useId',
|
|
22
|
+
'useEffectEvent',
|
|
23
|
+
'useDeferredValue',
|
|
24
|
+
'useTransition',
|
|
25
|
+
'useSyncExternalStore',
|
|
26
|
+
'useActionState',
|
|
27
|
+
'useFormState',
|
|
28
|
+
'useFormStatus',
|
|
29
|
+
'useOptimistic',
|
|
30
|
+
]);
|
|
16
31
|
const FUNCTION_TYPES = new Set([
|
|
17
32
|
'FunctionDeclaration',
|
|
18
33
|
'FunctionExpression',
|
|
@@ -65,6 +80,8 @@ export const STRONG_RENDER_IMPURE_CALL = 'OCTANE_STRONG_RENDER_IMPURE_CALL';
|
|
|
65
80
|
export const STRONG_RENDER_EFFECT_EVENT_CALL = 'OCTANE_STRONG_RENDER_EFFECT_EVENT_CALL';
|
|
66
81
|
export const STRONG_EFFECT_EVENT_DEPENDENCY = 'OCTANE_STRONG_EFFECT_EVENT_DEPENDENCY';
|
|
67
82
|
export const STRONG_DIRECTIVE_PLACEMENT = 'OCTANE_STRONG_DIRECTIVE_PLACEMENT';
|
|
83
|
+
export const STRONG_HOOK_LOCALITY = 'OCTANE_STRONG_HOOK_LOCALITY';
|
|
84
|
+
export const STRONG_EVENT_HANDLER_LOCALITY = 'OCTANE_STRONG_EVENT_HANDLER_LOCALITY';
|
|
68
85
|
|
|
69
86
|
function primitiveValueMask(value) {
|
|
70
87
|
return value === undefined
|
|
@@ -342,6 +359,812 @@ function strongDirectives(ast) {
|
|
|
342
359
|
return { enabled, misplaced };
|
|
343
360
|
}
|
|
344
361
|
|
|
362
|
+
// Template arms create their own hook lifetimes. A value used only by one arm
|
|
363
|
+
// can be declared there; a value used by siblings belongs to their parent.
|
|
364
|
+
// Keep this source-level check separate from the phase analysis above: it never
|
|
365
|
+
// annotates the parser tree or changes the code emitted for a valid module.
|
|
366
|
+
function strongLocalityDiagnostics(ast, source, filename, isReassigned) {
|
|
367
|
+
const diagnostics = [];
|
|
368
|
+
const values = [];
|
|
369
|
+
const effects = [];
|
|
370
|
+
const handlers = [];
|
|
371
|
+
const effectByCall = new WeakMap();
|
|
372
|
+
const valueByCall = new WeakMap();
|
|
373
|
+
const hostAttributes = new WeakSet();
|
|
374
|
+
const componentHandlers = new WeakSet();
|
|
375
|
+
const moduleScope = createScope(null, 'module', ast.body ?? []);
|
|
376
|
+
const mayHaveHoistedVars = source.includes('var');
|
|
377
|
+
let activeHandler = null;
|
|
378
|
+
let activeDerivedValue = null;
|
|
379
|
+
|
|
380
|
+
function predeclareNestedVars(node, functionScope, root = true) {
|
|
381
|
+
if (node == null || typeof node !== 'object') return;
|
|
382
|
+
if (Array.isArray(node)) {
|
|
383
|
+
for (const child of node) predeclareNestedVars(child, functionScope, false);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
FUNCTION_TYPES.has(node.type) ||
|
|
388
|
+
(!root && node.type === 'JSXCodeBlock') ||
|
|
389
|
+
node.type === 'JSXIfExpression' ||
|
|
390
|
+
node.type === 'JSXForExpression' ||
|
|
391
|
+
node.type === 'JSXSwitchExpression' ||
|
|
392
|
+
node.type === 'JSXTryExpression' ||
|
|
393
|
+
node.type === 'ClassDeclaration' ||
|
|
394
|
+
node.type === 'ClassExpression' ||
|
|
395
|
+
node.type === 'StaticBlock' ||
|
|
396
|
+
(node.type?.startsWith('TS') && !TRANSPARENT_EXPRESSIONS.has(node.type))
|
|
397
|
+
)
|
|
398
|
+
return;
|
|
399
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'var') {
|
|
400
|
+
for (const declaration of node.declarations ?? []) {
|
|
401
|
+
addPatternNames(declaration.id, functionScope.bindings, OTHER_BINDING, false);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const key in node) {
|
|
405
|
+
if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) {
|
|
406
|
+
predeclareNestedVars(node[key], functionScope, false);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function templateArm(node, parent, label, sameLifetime = false) {
|
|
412
|
+
return { node, parent, label, sameLifetime };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function register(statements, scope, region) {
|
|
416
|
+
if (region === null) return;
|
|
417
|
+
for (const statement of statements ?? []) {
|
|
418
|
+
const node = declarationOf(statement);
|
|
419
|
+
if (node?.type === 'FunctionDeclaration' && node.id?.name) {
|
|
420
|
+
const handler = { kind: 'locality-handler', node, region, uses: [] };
|
|
421
|
+
scope.bindings.set(node.id.name, handler);
|
|
422
|
+
handlers.push(handler);
|
|
423
|
+
} else if (node?.type === 'VariableDeclaration' && node.kind === 'const') {
|
|
424
|
+
for (const declaration of node.declarations ?? []) {
|
|
425
|
+
const init = unwrap(declaration.init);
|
|
426
|
+
const hook = init?.type === 'CallExpression' ? importedHook(init.callee, scope) : null;
|
|
427
|
+
if (hook !== null && LOCAL_VALUE_HOOKS.has(hook)) {
|
|
428
|
+
const value = {
|
|
429
|
+
kind: 'locality-value',
|
|
430
|
+
node: init,
|
|
431
|
+
hook,
|
|
432
|
+
region,
|
|
433
|
+
uses: [],
|
|
434
|
+
dependencies: new Set(),
|
|
435
|
+
helpers: new Set(),
|
|
436
|
+
};
|
|
437
|
+
valueByCall.set(init, value);
|
|
438
|
+
addPatternNames(declaration.id, scope.bindings, value);
|
|
439
|
+
values.push(value);
|
|
440
|
+
} else if (declaration.id?.type === 'Identifier' && FUNCTION_TYPES.has(init?.type)) {
|
|
441
|
+
const handler = { kind: 'locality-handler', node: declaration, region, uses: [] };
|
|
442
|
+
scope.bindings.set(declaration.id.name, handler);
|
|
443
|
+
handlers.push(handler);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} else if (node?.type === 'ExpressionStatement') {
|
|
447
|
+
const call = unwrap(node.expression);
|
|
448
|
+
if (call?.type !== 'CallExpression') continue;
|
|
449
|
+
const hook = importedHook(call.callee, scope);
|
|
450
|
+
if (!EFFECT_HOOKS.has(hook)) continue;
|
|
451
|
+
const effect = { node: call, hook, region, dependencies: new Set(), helpers: new Set() };
|
|
452
|
+
effectByCall.set(call, effect);
|
|
453
|
+
effects.push(effect);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function block(body, parentScope, region, effect = null, template = false, event = null) {
|
|
459
|
+
const scope = createScope(parentScope, template ? 'function' : 'block', body?.body ?? []);
|
|
460
|
+
if (template && mayHaveHoistedVars) predeclareNestedVars(body, scope);
|
|
461
|
+
register(body?.body, scope, region);
|
|
462
|
+
for (const statement of body?.body ?? []) walk(statement, scope, region, effect, event);
|
|
463
|
+
if (body?.render) walk(body.render, scope, region, effect, event);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function arm(body, scope, parent, directive, label) {
|
|
467
|
+
if (body == null) return;
|
|
468
|
+
if (body.type === 'BlockStatement' || body.type === 'JSXCodeBlock') {
|
|
469
|
+
block(body, scope, templateArm(directive, parent, label), null, true);
|
|
470
|
+
} else {
|
|
471
|
+
walk(body, scope, parent);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function conditionalArms(node, scope, region, effect) {
|
|
476
|
+
walk(node.test, scope, region, effect);
|
|
477
|
+
arm(node.consequent, scope, region, node, '@if arm');
|
|
478
|
+
if (node.alternate?.type === 'IfStatement') {
|
|
479
|
+
conditionalArms(node.alternate, scope, region, effect);
|
|
480
|
+
} else {
|
|
481
|
+
arm(node.alternate, scope, region, node, '@else arm');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function useBinding(name, scope, region, effect, event) {
|
|
486
|
+
const binding = resolve(scope, name);
|
|
487
|
+
if (binding?.kind === 'locality-value') {
|
|
488
|
+
if (effect !== null) {
|
|
489
|
+
effect.dependencies.add(binding);
|
|
490
|
+
} else if (activeDerivedValue !== null && activeDerivedValue !== binding) {
|
|
491
|
+
activeDerivedValue.dependencies.add(binding);
|
|
492
|
+
} else {
|
|
493
|
+
binding.uses.push(activeHandler ? { viaHandler: activeHandler } : region);
|
|
494
|
+
}
|
|
495
|
+
} else if (binding?.kind === 'locality-handler') {
|
|
496
|
+
if (activeDerivedValue !== null && effect === null) {
|
|
497
|
+
binding.uses.push({ region, viaHandler: activeHandler, viaDerived: activeDerivedValue });
|
|
498
|
+
activeDerivedValue.helpers.add(binding);
|
|
499
|
+
} else {
|
|
500
|
+
binding.uses.push({ region, event, viaHandler: activeHandler, effect });
|
|
501
|
+
if (effect !== null) effect.helpers.add(binding);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function ownsTemplate(body, name) {
|
|
507
|
+
body = unwrap(body);
|
|
508
|
+
if (body?.type === 'JSXCodeBlock') return true;
|
|
509
|
+
if (!/^[A-Z]/.test(name ?? '')) return false;
|
|
510
|
+
if (body?.type === 'JSXElement' || body?.type === 'JSXFragment') return true;
|
|
511
|
+
return (
|
|
512
|
+
body?.type === 'BlockStatement' &&
|
|
513
|
+
(body.body ?? []).some((statement) => {
|
|
514
|
+
const argument = statement.type === 'ReturnStatement' ? unwrap(statement.argument) : null;
|
|
515
|
+
return argument?.type === 'JSXElement' || argument?.type === 'JSXFragment';
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function walkPatternExpressions(pattern, scope, region, effect) {
|
|
521
|
+
if (pattern == null) return;
|
|
522
|
+
switch (pattern.type) {
|
|
523
|
+
case 'AssignmentPattern':
|
|
524
|
+
walkPatternExpressions(pattern.left, scope, region, effect);
|
|
525
|
+
walk(pattern.right, scope, region, effect);
|
|
526
|
+
return;
|
|
527
|
+
case 'ArrayPattern':
|
|
528
|
+
for (const element of pattern.elements ?? []) {
|
|
529
|
+
walkPatternExpressions(element, scope, region, effect);
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
case 'ObjectPattern':
|
|
533
|
+
for (const property of pattern.properties ?? []) {
|
|
534
|
+
if (property.type === 'Property') {
|
|
535
|
+
if (property.computed) walk(property.key, scope, region, effect);
|
|
536
|
+
walkPatternExpressions(property.value, scope, region, effect);
|
|
537
|
+
} else walkPatternExpressions(property, scope, region, effect);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
case 'RestElement':
|
|
541
|
+
walkPatternExpressions(pattern.argument, scope, region, effect);
|
|
542
|
+
return;
|
|
543
|
+
case 'TSParameterProperty':
|
|
544
|
+
walkPatternExpressions(pattern.parameter, scope, region, effect);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function walk(node, scope, region, effect = null, event = null) {
|
|
550
|
+
if (node == null || typeof node !== 'object') return;
|
|
551
|
+
if (Array.isArray(node)) {
|
|
552
|
+
for (const child of node) walk(child, scope, region, effect, event);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (TRANSPARENT_EXPRESSIONS.has(node.type)) {
|
|
556
|
+
walk(node.expression, scope, region, effect, event);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (node.type === 'TSEnumDeclaration') {
|
|
560
|
+
for (const member of node.body?.members ?? node.members ?? []) {
|
|
561
|
+
walk(member.initializer, scope, region, effect);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (node.type === 'TSModuleDeclaration') {
|
|
566
|
+
walk(node.body, scope, region, effect);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (node.type === 'TSModuleBlock') {
|
|
570
|
+
const namespaceScope = createScope(scope, 'block', node.body ?? []);
|
|
571
|
+
for (const statement of node.body ?? []) walk(statement, namespaceScope, region, effect);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (node.type?.startsWith('TS')) return;
|
|
575
|
+
switch (node.type) {
|
|
576
|
+
case 'ImportDeclaration':
|
|
577
|
+
case 'JSXIdentifier':
|
|
578
|
+
case 'Literal':
|
|
579
|
+
return;
|
|
580
|
+
case 'Identifier': {
|
|
581
|
+
useBinding(node.name, scope, region, effect, event);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
case 'ExportNamedDeclaration':
|
|
585
|
+
case 'ExportDefaultDeclaration':
|
|
586
|
+
walk(node.declaration, scope, region, effect);
|
|
587
|
+
return;
|
|
588
|
+
case 'FunctionDeclaration':
|
|
589
|
+
case 'FunctionExpression':
|
|
590
|
+
case 'ArrowFunctionExpression': {
|
|
591
|
+
const previousHandler = activeHandler;
|
|
592
|
+
const declared =
|
|
593
|
+
node.type === 'FunctionDeclaration' && node.id?.name
|
|
594
|
+
? resolve(scope, node.id.name)
|
|
595
|
+
: null;
|
|
596
|
+
const ownTemplate = ownsTemplate(node.body, node.id?.name ?? activeHandler?.node.id?.name);
|
|
597
|
+
if (ownTemplate) activeHandler = null;
|
|
598
|
+
else if (declared?.kind === 'locality-handler') activeHandler = declared;
|
|
599
|
+
try {
|
|
600
|
+
const body = node.body;
|
|
601
|
+
// A new component owns its own template; ordinary closures retain the
|
|
602
|
+
// owner's region so inline event callbacks count as local uses.
|
|
603
|
+
const owner =
|
|
604
|
+
ownTemplate || region === null ? templateArm(node, null, 'component') : region;
|
|
605
|
+
const statements =
|
|
606
|
+
body?.type === 'BlockStatement' || body?.type === 'JSXCodeBlock' ? body.body : [];
|
|
607
|
+
const functionScope = createScope(scope, 'function', statements, node.params ?? []);
|
|
608
|
+
if (mayHaveHoistedVars) predeclareNestedVars(body, functionScope);
|
|
609
|
+
if (node.id?.name) functionScope.bindings.set(node.id.name, OTHER_BINDING);
|
|
610
|
+
if (ownTemplate || region === null) register(statements, functionScope, owner);
|
|
611
|
+
const parameterScope = createScope(scope, 'function', [], node.params ?? []);
|
|
612
|
+
for (const parameter of node.params ?? []) {
|
|
613
|
+
walkPatternExpressions(parameter, parameterScope, region, effect);
|
|
614
|
+
}
|
|
615
|
+
if (body?.type === 'BlockStatement' || body?.type === 'JSXCodeBlock') {
|
|
616
|
+
for (const statement of statements ?? [])
|
|
617
|
+
walk(statement, functionScope, owner, effect, event);
|
|
618
|
+
if (body.render) walk(body.render, functionScope, owner, effect, event);
|
|
619
|
+
} else {
|
|
620
|
+
walk(body, functionScope, owner, effect, event);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
} finally {
|
|
624
|
+
activeHandler = previousHandler;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
case 'BlockStatement':
|
|
628
|
+
block(node, scope, region, effect, false, event);
|
|
629
|
+
return;
|
|
630
|
+
case 'JSXCodeBlock':
|
|
631
|
+
block(node, scope, templateArm(node, region, '@{} block', true), effect, true, event);
|
|
632
|
+
return;
|
|
633
|
+
case 'CatchClause': {
|
|
634
|
+
const catchScope = createScope(scope, 'block', [], [node.param]);
|
|
635
|
+
walkPatternExpressions(node.param, catchScope, region, effect);
|
|
636
|
+
walk(node.body, catchScope, region, effect);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
case 'ForStatement': {
|
|
640
|
+
const loopScope = createScope(
|
|
641
|
+
scope,
|
|
642
|
+
'block',
|
|
643
|
+
node.init?.type === 'VariableDeclaration' ? [node.init] : [],
|
|
644
|
+
);
|
|
645
|
+
walk(node.init, loopScope, region, effect);
|
|
646
|
+
walk(node.test, loopScope, region, effect);
|
|
647
|
+
walk(node.update, loopScope, region, effect);
|
|
648
|
+
walk(node.body, loopScope, region, effect);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
case 'ForInStatement':
|
|
652
|
+
case 'ForOfStatement': {
|
|
653
|
+
walk(node.right, scope, region, effect);
|
|
654
|
+
const loopScope = createScope(
|
|
655
|
+
scope,
|
|
656
|
+
'block',
|
|
657
|
+
node.left?.type === 'VariableDeclaration' ? [node.left] : [],
|
|
658
|
+
);
|
|
659
|
+
walk(node.left, loopScope, region, effect);
|
|
660
|
+
walk(node.body, loopScope, region, effect);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
case 'SwitchStatement': {
|
|
664
|
+
walk(node.discriminant, scope, region, effect);
|
|
665
|
+
const statements = (node.cases ?? []).flatMap((branch) => branch.consequent ?? []);
|
|
666
|
+
const switchScope = createScope(scope, 'block', statements);
|
|
667
|
+
register(statements, switchScope, region);
|
|
668
|
+
for (const branch of node.cases ?? []) {
|
|
669
|
+
walk(branch.test, switchScope, region, effect);
|
|
670
|
+
for (const statement of branch.consequent ?? []) {
|
|
671
|
+
walk(statement, switchScope, region, effect);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
case 'StaticBlock': {
|
|
677
|
+
const staticScope = createScope(scope, 'block', node.body ?? []);
|
|
678
|
+
register(node.body, staticScope, region);
|
|
679
|
+
for (const statement of node.body ?? []) walk(statement, staticScope, region, effect);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
case 'ClassDeclaration':
|
|
683
|
+
case 'ClassExpression': {
|
|
684
|
+
walk(node.superClass, scope, region, effect);
|
|
685
|
+
const classScope = createScope(scope, 'block', [], node.id ? [node.id] : []);
|
|
686
|
+
walk(node.body, classScope, region, effect);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
case 'MethodDefinition':
|
|
690
|
+
case 'PropertyDefinition':
|
|
691
|
+
case 'AccessorProperty':
|
|
692
|
+
if (node.computed) walk(node.key, scope, region, effect);
|
|
693
|
+
walk(node.value, scope, region, effect);
|
|
694
|
+
return;
|
|
695
|
+
case 'LabeledStatement':
|
|
696
|
+
walk(node.body, scope, region, effect);
|
|
697
|
+
return;
|
|
698
|
+
case 'BreakStatement':
|
|
699
|
+
case 'ContinueStatement':
|
|
700
|
+
return;
|
|
701
|
+
case 'JSXIfExpression':
|
|
702
|
+
conditionalArms(node, scope, region, effect);
|
|
703
|
+
return;
|
|
704
|
+
case 'JSXForExpression': {
|
|
705
|
+
walk(node.right, scope, region, effect);
|
|
706
|
+
const rowScope = createScope(scope, 'function', [node.left]);
|
|
707
|
+
addPatternNames(node.index, rowScope.bindings, OTHER_BINDING);
|
|
708
|
+
for (const declaration of node.left?.declarations ?? []) {
|
|
709
|
+
walkPatternExpressions(declaration.id, rowScope, region, effect);
|
|
710
|
+
}
|
|
711
|
+
walkPatternExpressions(node.index, rowScope, region, effect);
|
|
712
|
+
walk(node.key, rowScope, region, effect);
|
|
713
|
+
arm(node.body, rowScope, region, node, '@for row');
|
|
714
|
+
arm(node.empty, scope, region, node, '@empty arm');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
case 'JSXSwitchExpression':
|
|
718
|
+
walk(node.discriminant, scope, region, effect);
|
|
719
|
+
for (const branch of node.cases ?? []) {
|
|
720
|
+
walk(branch.test, scope, region, effect);
|
|
721
|
+
const owner = templateArm(
|
|
722
|
+
branch,
|
|
723
|
+
region,
|
|
724
|
+
branch.test == null ? '@default arm' : '@case arm',
|
|
725
|
+
);
|
|
726
|
+
const caseScope = createScope(scope, 'function', branch.consequent ?? []);
|
|
727
|
+
if (mayHaveHoistedVars) predeclareNestedVars(branch.consequent, caseScope);
|
|
728
|
+
register(branch.consequent, caseScope, owner);
|
|
729
|
+
for (const statement of branch.consequent ?? []) walk(statement, caseScope, owner);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
case 'JSXTryExpression':
|
|
733
|
+
arm(node.block, scope, region, node, '@try arm');
|
|
734
|
+
arm(node.pending, scope, region, node, '@pending arm');
|
|
735
|
+
if (node.handler) {
|
|
736
|
+
const catchScope = createScope(
|
|
737
|
+
scope,
|
|
738
|
+
'block',
|
|
739
|
+
[],
|
|
740
|
+
[node.handler.param, node.handler.resetParam],
|
|
741
|
+
);
|
|
742
|
+
arm(node.handler.body, catchScope, region, node, '@catch arm');
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
case 'VariableDeclaration':
|
|
746
|
+
for (const declaration of node.declarations ?? []) {
|
|
747
|
+
walkPatternExpressions(declaration.id, scope, region, effect);
|
|
748
|
+
const previousHandler = activeHandler;
|
|
749
|
+
const declared =
|
|
750
|
+
declaration.id?.type === 'Identifier' ? resolve(scope, declaration.id.name) : null;
|
|
751
|
+
if (declared?.kind === 'locality-handler' && declared.node === declaration) {
|
|
752
|
+
activeHandler = declared;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
walk(declaration.init, scope, region, effect);
|
|
756
|
+
} finally {
|
|
757
|
+
activeHandler = previousHandler;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
case 'Property':
|
|
762
|
+
if (node.computed) walk(node.key, scope, region, effect);
|
|
763
|
+
walk(node.value, scope, region, effect);
|
|
764
|
+
return;
|
|
765
|
+
case 'MemberExpression':
|
|
766
|
+
walk(node.object, scope, region, effect);
|
|
767
|
+
if (node.computed) walk(node.property, scope, region, effect);
|
|
768
|
+
return;
|
|
769
|
+
case 'JSXElement': {
|
|
770
|
+
const name = node.openingElement?.name;
|
|
771
|
+
if (name?.type === 'JSXIdentifier' && /^[a-z]/.test(name.name)) {
|
|
772
|
+
for (const attribute of node.openingElement.attributes ?? []) {
|
|
773
|
+
if (attribute.type === 'JSXAttribute') hostAttributes.add(attribute);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
walk(node.openingElement, scope, region, effect);
|
|
777
|
+
walk(node.children, scope, region, effect);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
case 'JSXOpeningElement': {
|
|
781
|
+
let tag = node.name;
|
|
782
|
+
const member = tag?.type === 'JSXMemberExpression';
|
|
783
|
+
while (tag?.type === 'JSXMemberExpression') tag = tag.object;
|
|
784
|
+
if (tag?.type === 'JSXIdentifier' && (member || /^[A-Z]/.test(tag.name))) {
|
|
785
|
+
const binding = resolve(scope, tag.name);
|
|
786
|
+
if (!member && binding?.kind === 'locality-handler') componentHandlers.add(binding);
|
|
787
|
+
useBinding(tag.name, scope, region, effect, null);
|
|
788
|
+
}
|
|
789
|
+
walk(node.attributes, scope, region, effect);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
case 'JSXAttribute': {
|
|
793
|
+
const name = node.name?.name;
|
|
794
|
+
const expression = unwrap(node.value?.expression);
|
|
795
|
+
const attribute =
|
|
796
|
+
hostAttributes.has(node) &&
|
|
797
|
+
typeof name === 'string' &&
|
|
798
|
+
/^on[A-Z]/.test(name) &&
|
|
799
|
+
(expression?.type === 'Identifier' || FUNCTION_TYPES.has(expression?.type))
|
|
800
|
+
? node
|
|
801
|
+
: null;
|
|
802
|
+
walk(node.value, scope, region, effect, attribute);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
case 'CallExpression': {
|
|
806
|
+
const localEffect = effectByCall.get(node) ?? effect;
|
|
807
|
+
const previousDerived = activeDerivedValue;
|
|
808
|
+
activeDerivedValue = valueByCall.get(node) ?? previousDerived;
|
|
809
|
+
try {
|
|
810
|
+
walk(node.callee, scope, region, localEffect, event);
|
|
811
|
+
walk(node.arguments, scope, region, localEffect, event);
|
|
812
|
+
} finally {
|
|
813
|
+
activeDerivedValue = previousDerived;
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
case 'MetaProperty':
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
for (const key in node) {
|
|
821
|
+
if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) {
|
|
822
|
+
walk(node[key], scope, region, effect, event);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
for (const statement of ast.body ?? []) walk(statement, moduleScope, null);
|
|
828
|
+
|
|
829
|
+
function within(region, ancestor) {
|
|
830
|
+
for (let current = region; current !== null; current = current.parent) {
|
|
831
|
+
if (current === ancestor) return true;
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// A hook, the callbacks that capture it, and effects that observe it must
|
|
837
|
+
// agree on one owner. Grouping these dependencies once avoids repeatedly
|
|
838
|
+
// recalculating every owner for a chain of effects.
|
|
839
|
+
// Only referenced helpers constrain ownership. An unused closure can read
|
|
840
|
+
// several hooks needed by separate blocks without sharing their lifetime.
|
|
841
|
+
const consumedHandlers = new Set();
|
|
842
|
+
const capturedHandlers = new Map();
|
|
843
|
+
for (const handler of handlers) {
|
|
844
|
+
if (componentHandlers.has(handler)) consumedHandlers.add(handler);
|
|
845
|
+
for (const use of handler.uses) {
|
|
846
|
+
if (use.viaHandler) {
|
|
847
|
+
let captured = capturedHandlers.get(use.viaHandler);
|
|
848
|
+
if (captured === undefined) capturedHandlers.set(use.viaHandler, (captured = []));
|
|
849
|
+
captured.push(handler);
|
|
850
|
+
} else {
|
|
851
|
+
consumedHandlers.add(handler);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const reachable = [...consumedHandlers];
|
|
856
|
+
for (let index = 0; index < reachable.length; index++) {
|
|
857
|
+
for (const captured of capturedHandlers.get(reachable[index]) ?? []) {
|
|
858
|
+
if (consumedHandlers.has(captured)) continue;
|
|
859
|
+
consumedHandlers.add(captured);
|
|
860
|
+
reachable.push(captured);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const unusedCaptures = new Map();
|
|
864
|
+
const nodes = [...values, ...effects, ...handlers];
|
|
865
|
+
const edges = new Map(nodes.map((node) => [node, new Set()]));
|
|
866
|
+
const directUses = new Map(nodes.map((node) => [node, []]));
|
|
867
|
+
function connect(left, right) {
|
|
868
|
+
if (left === right || !edges.has(left) || !edges.has(right)) return;
|
|
869
|
+
edges.get(left).add(right);
|
|
870
|
+
edges.get(right).add(left);
|
|
871
|
+
}
|
|
872
|
+
for (const value of values) {
|
|
873
|
+
for (const use of value.uses) {
|
|
874
|
+
if (use?.viaHandler) {
|
|
875
|
+
if (consumedHandlers.has(use.viaHandler)) {
|
|
876
|
+
connect(value, use.viaHandler);
|
|
877
|
+
} else {
|
|
878
|
+
let unused = unusedCaptures.get(value);
|
|
879
|
+
if (unused === undefined) unusedCaptures.set(value, (unused = new Set()));
|
|
880
|
+
unused.add(use.viaHandler);
|
|
881
|
+
}
|
|
882
|
+
} else directUses.get(value).push(use?.region ?? use);
|
|
883
|
+
}
|
|
884
|
+
for (const dependency of value.dependencies) connect(value, dependency);
|
|
885
|
+
for (const helper of value.helpers) connect(value, helper);
|
|
886
|
+
}
|
|
887
|
+
for (const effect of effects) {
|
|
888
|
+
for (const dependency of effect.dependencies) connect(effect, dependency);
|
|
889
|
+
for (const helper of effect.helpers) connect(effect, helper);
|
|
890
|
+
// An effect already declared in a nested block uses its dependencies
|
|
891
|
+
// there. A parent effect can instead move with its dependencies.
|
|
892
|
+
if (effect.region?.sameLifetime) directUses.get(effect).push(effect.region);
|
|
893
|
+
}
|
|
894
|
+
for (const handler of handlers) {
|
|
895
|
+
for (const use of handler.uses) {
|
|
896
|
+
if (use.viaHandler) {
|
|
897
|
+
if (consumedHandlers.has(use.viaHandler)) connect(handler, use.viaHandler);
|
|
898
|
+
} else if (use.viaDerived) connect(handler, use.viaDerived);
|
|
899
|
+
else if (use.effect) connect(handler, use.effect);
|
|
900
|
+
else directUses.get(handler).push(use.region);
|
|
901
|
+
}
|
|
902
|
+
// An unused helper does not consume its captured hooks in this region;
|
|
903
|
+
// keep an actual component reference anchored to its own lifetime.
|
|
904
|
+
if (componentHandlers.has(handler)) {
|
|
905
|
+
directUses.get(handler).push(handler.region);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function commonAncestor(left, right) {
|
|
910
|
+
for (let current = left; current !== null; current = current.parent) {
|
|
911
|
+
if (within(right, current)) return current;
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function canMoveInto(owner, declarationRegion) {
|
|
917
|
+
for (let current = owner; current !== null; current = current.parent) {
|
|
918
|
+
if (current === declarationRegion) return true;
|
|
919
|
+
// A directive arm or keyed row changes hook lifetime. Only a
|
|
920
|
+
// nested @{} block within the same lifetime can own this value.
|
|
921
|
+
if (!current.sameLifetime) return false;
|
|
922
|
+
}
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const visited = new Set();
|
|
927
|
+
for (const node of nodes) {
|
|
928
|
+
if (visited.has(node)) continue;
|
|
929
|
+
const group = [];
|
|
930
|
+
const pending = [node];
|
|
931
|
+
visited.add(node);
|
|
932
|
+
while (pending.length > 0) {
|
|
933
|
+
const member = pending.pop();
|
|
934
|
+
group.push(member);
|
|
935
|
+
for (const neighbor of edges.get(member)) {
|
|
936
|
+
if (visited.has(neighbor)) continue;
|
|
937
|
+
visited.add(neighbor);
|
|
938
|
+
pending.push(neighbor);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
let common;
|
|
942
|
+
for (const member of group) {
|
|
943
|
+
for (const use of directUses.get(member)) {
|
|
944
|
+
common = common === undefined ? use : commonAncestor(common, use);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (common == null) continue;
|
|
948
|
+
let owner = common;
|
|
949
|
+
while (owner !== null && !group.every((member) => canMoveInto(owner, member.region))) {
|
|
950
|
+
owner = owner.parent;
|
|
951
|
+
}
|
|
952
|
+
if (owner === null) continue;
|
|
953
|
+
// A rewritten function declaration is not a stable capture of the hook
|
|
954
|
+
// values seen in its original body. Check this only for candidate moves,
|
|
955
|
+
// leaving the expensive reassignment scan lazy on ordinary modules.
|
|
956
|
+
if (
|
|
957
|
+
group.some(
|
|
958
|
+
(member) =>
|
|
959
|
+
member.kind === 'locality-handler' &&
|
|
960
|
+
member.region !== owner &&
|
|
961
|
+
member.node.type === 'FunctionDeclaration' &&
|
|
962
|
+
isReassigned(member.node.id),
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
continue;
|
|
966
|
+
|
|
967
|
+
const toMove = group.filter(
|
|
968
|
+
(member) => member.kind === 'locality-handler' && member.region !== owner,
|
|
969
|
+
);
|
|
970
|
+
const names = toMove
|
|
971
|
+
.slice(0, 3)
|
|
972
|
+
.map((helper) => helper.node.id?.name)
|
|
973
|
+
.filter(Boolean);
|
|
974
|
+
const helperText =
|
|
975
|
+
toMove.length === 0
|
|
976
|
+
? ''
|
|
977
|
+
: ` and ${toMove.length === 1 ? 'its helper' : 'its helpers'} ${names.join(', ')}${toMove.length > 3 ? ` and ${toMove.length - 3} more` : ''}`;
|
|
978
|
+
for (const member of group) {
|
|
979
|
+
if (member.region === owner) continue;
|
|
980
|
+
if (member.kind === 'locality-value') {
|
|
981
|
+
const unused = unusedCaptures.get(member);
|
|
982
|
+
const unusedNames = unused
|
|
983
|
+
? [...unused].slice(0, 3).map((helper) => helper.node.id.name)
|
|
984
|
+
: [];
|
|
985
|
+
const unusedText =
|
|
986
|
+
unusedNames.length === 0
|
|
987
|
+
? ''
|
|
988
|
+
: ` Remove unused ${unused.size === 1 ? 'helper' : 'helpers'} ${unusedNames.join(', ')}${unused.size > 3 ? ` and ${unused.size - 3} more` : ''}, which still ${unused.size === 1 ? 'captures' : 'capture'} this value, before moving it.`;
|
|
989
|
+
diagnostics.push(
|
|
990
|
+
diagnostic(
|
|
991
|
+
STRONG_HOOK_LOCALITY,
|
|
992
|
+
filename,
|
|
993
|
+
member.node,
|
|
994
|
+
`Move ${member.hook}${helperText} into the ${owner.label} at line ${owner.node.loc?.start?.line ?? 1}, before the JSX or local effect that uses its value.${unusedText}`,
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
} else if (
|
|
998
|
+
member.kind !== 'locality-handler' &&
|
|
999
|
+
(member.dependencies.size > 0 || member.helpers.size > 0)
|
|
1000
|
+
) {
|
|
1001
|
+
diagnostics.push(
|
|
1002
|
+
diagnostic(
|
|
1003
|
+
STRONG_HOOK_LOCALITY,
|
|
1004
|
+
filename,
|
|
1005
|
+
member.node,
|
|
1006
|
+
`Move ${member.hook} into the ${owner.label} at line ${owner.node.loc?.start?.line ?? 1}, beside the local hook values it reads and before that scope's JSX output.`,
|
|
1007
|
+
),
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// A callback may safely move beside its sole event even when a hook it
|
|
1013
|
+
// captures is also consumed by the parent or by a sibling block. Determine
|
|
1014
|
+
// handler placement from handler uses separately from hook ownership.
|
|
1015
|
+
const eventHandlers = handlers.filter((handler) => handler.uses.some((use) => use.event != null));
|
|
1016
|
+
if (eventHandlers.length === 0) return diagnostics;
|
|
1017
|
+
|
|
1018
|
+
function mergeOwner(left, right) {
|
|
1019
|
+
if (left === undefined) return right;
|
|
1020
|
+
if (right === undefined) return left;
|
|
1021
|
+
return commonAncestor(left, right);
|
|
1022
|
+
}
|
|
1023
|
+
const eventOwners = new Map();
|
|
1024
|
+
if (
|
|
1025
|
+
eventHandlers.length <= 2 ||
|
|
1026
|
+
!handlers.some((handler) => handler.uses.some((use) => use.viaHandler))
|
|
1027
|
+
) {
|
|
1028
|
+
// A couple of walks, or independent handlers, cost less than building
|
|
1029
|
+
// a graph in the common case.
|
|
1030
|
+
for (const handler of eventHandlers) {
|
|
1031
|
+
const seen = new Set();
|
|
1032
|
+
const pending = [handler];
|
|
1033
|
+
let owner;
|
|
1034
|
+
while (pending.length > 0) {
|
|
1035
|
+
const current = pending.pop();
|
|
1036
|
+
if (seen.has(current)) continue;
|
|
1037
|
+
seen.add(current);
|
|
1038
|
+
if (componentHandlers.has(current) || current.uses.length === 0) {
|
|
1039
|
+
owner = mergeOwner(owner, current.region);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
for (const use of current.uses) {
|
|
1043
|
+
if (use.viaHandler) pending.push(use.viaHandler);
|
|
1044
|
+
else owner = mergeOwner(owner, use.viaDerived ? current.region : use.region);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
eventOwners.set(handler, owner);
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
// A helper can be used by several callbacks, including a recursive cycle.
|
|
1051
|
+
// Collapse those cycles, then inherit the common use region through the
|
|
1052
|
+
// resulting DAG once instead of retraversing a chain for every event.
|
|
1053
|
+
const handlerIndex = new Map(handlers.map((handler, index) => [handler, index]));
|
|
1054
|
+
const dependencies = handlers.map(() => []);
|
|
1055
|
+
const dependents = handlers.map(() => []);
|
|
1056
|
+
const directOwners = new Array(handlers.length);
|
|
1057
|
+
for (let index = 0; index < handlers.length; index++) {
|
|
1058
|
+
const handler = handlers[index];
|
|
1059
|
+
if (componentHandlers.has(handler) || handler.uses.length === 0) {
|
|
1060
|
+
directOwners[index] = handler.region;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
for (const use of handler.uses) {
|
|
1064
|
+
if (use.viaHandler) {
|
|
1065
|
+
const target = handlerIndex.get(use.viaHandler);
|
|
1066
|
+
if (target !== undefined) {
|
|
1067
|
+
dependencies[index].push(target);
|
|
1068
|
+
dependents[target].push(index);
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
directOwners[index] = mergeOwner(
|
|
1072
|
+
directOwners[index],
|
|
1073
|
+
use.viaDerived ? handler.region : use.region,
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const visitedHandlers = new Uint8Array(handlers.length);
|
|
1079
|
+
const nextDependency = new Uint32Array(handlers.length);
|
|
1080
|
+
const finishOrder = [];
|
|
1081
|
+
for (let index = 0; index < handlers.length; index++) {
|
|
1082
|
+
if (visitedHandlers[index]) continue;
|
|
1083
|
+
visitedHandlers[index] = 1;
|
|
1084
|
+
const pending = [index];
|
|
1085
|
+
while (pending.length > 0) {
|
|
1086
|
+
const current = pending[pending.length - 1];
|
|
1087
|
+
const target = dependencies[current][nextDependency[current]++];
|
|
1088
|
+
if (target !== undefined) {
|
|
1089
|
+
if (!visitedHandlers[target]) {
|
|
1090
|
+
visitedHandlers[target] = 1;
|
|
1091
|
+
pending.push(target);
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
finishOrder.push(current);
|
|
1095
|
+
pending.pop();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const componentByHandler = new Int32Array(handlers.length).fill(-1);
|
|
1100
|
+
const componentOwners = [];
|
|
1101
|
+
for (let index = finishOrder.length - 1; index >= 0; index--) {
|
|
1102
|
+
const start = finishOrder[index];
|
|
1103
|
+
if (componentByHandler[start] !== -1) continue;
|
|
1104
|
+
const component = componentOwners.length;
|
|
1105
|
+
let owner;
|
|
1106
|
+
const pending = [start];
|
|
1107
|
+
componentByHandler[start] = component;
|
|
1108
|
+
while (pending.length > 0) {
|
|
1109
|
+
const current = pending.pop();
|
|
1110
|
+
owner = mergeOwner(owner, directOwners[current]);
|
|
1111
|
+
for (const previous of dependents[current]) {
|
|
1112
|
+
if (componentByHandler[previous] !== -1) continue;
|
|
1113
|
+
componentByHandler[previous] = component;
|
|
1114
|
+
pending.push(previous);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
componentOwners.push(owner);
|
|
1118
|
+
}
|
|
1119
|
+
const componentDependencies = componentOwners.map(() => new Set());
|
|
1120
|
+
const componentDependents = componentOwners.map(() => []);
|
|
1121
|
+
for (let index = 0; index < handlers.length; index++) {
|
|
1122
|
+
const from = componentByHandler[index];
|
|
1123
|
+
for (const target of dependencies[index]) {
|
|
1124
|
+
const to = componentByHandler[target];
|
|
1125
|
+
if (from === to || componentDependencies[from].has(to)) continue;
|
|
1126
|
+
componentDependencies[from].add(to);
|
|
1127
|
+
componentDependents[to].push(from);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const remainingDependencies = componentDependencies.map((targets) => targets.size);
|
|
1131
|
+
const ready = [];
|
|
1132
|
+
for (let index = 0; index < remainingDependencies.length; index++) {
|
|
1133
|
+
if (remainingDependencies[index] === 0) ready.push(index);
|
|
1134
|
+
}
|
|
1135
|
+
for (let index = 0; index < ready.length; index++) {
|
|
1136
|
+
const current = ready[index];
|
|
1137
|
+
for (const previous of componentDependents[current]) {
|
|
1138
|
+
componentOwners[previous] = mergeOwner(componentOwners[previous], componentOwners[current]);
|
|
1139
|
+
if (--remainingDependencies[previous] === 0) ready.push(previous);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
for (const handler of eventHandlers) {
|
|
1143
|
+
eventOwners.set(handler, componentOwners[componentByHandler[handlerIndex.get(handler)]]);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
for (const handler of eventHandlers) {
|
|
1147
|
+
let owner = eventOwners.get(handler);
|
|
1148
|
+
while (owner != null && !canMoveInto(owner, handler.region)) owner = owner.parent;
|
|
1149
|
+
if (owner == null || owner === handler.region) continue;
|
|
1150
|
+
if (handler.node.type === 'FunctionDeclaration' && isReassigned(handler.node.id)) continue;
|
|
1151
|
+
const name = handler.node.id?.name;
|
|
1152
|
+
const inlineAlternative =
|
|
1153
|
+
handler.uses.length === 1
|
|
1154
|
+
? ' Alternatively, define the callback inline at that event attribute.'
|
|
1155
|
+
: '';
|
|
1156
|
+
diagnostics.push(
|
|
1157
|
+
diagnostic(
|
|
1158
|
+
STRONG_EVENT_HANDLER_LOCALITY,
|
|
1159
|
+
filename,
|
|
1160
|
+
handler.node,
|
|
1161
|
+
`Move the ${name ?? 'event'} handler into the ${owner.label} at line ${owner.node.loc?.start?.line ?? 1}, before the JSX event attribute that uses it.${inlineAlternative}`,
|
|
1162
|
+
),
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return diagnostics;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
345
1168
|
/**
|
|
346
1169
|
* Analyze only modules that explicitly opted in. The authored parser tree is
|
|
347
1170
|
* read-only: scope information stays in local maps instead of annotating AST
|
|
@@ -384,6 +1207,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
384
1207
|
const hoistedVarNames = new WeakMap();
|
|
385
1208
|
const mayHaveHoistedVars = source.includes('var');
|
|
386
1209
|
const renderRoots = new WeakSet();
|
|
1210
|
+
let hasNestedCodeBlock = false;
|
|
387
1211
|
const renderOutputs = new WeakMap();
|
|
388
1212
|
function hasRenderOutput(fn) {
|
|
389
1213
|
if (!FUNCTION_TYPES.has(fn?.type)) return false;
|
|
@@ -449,14 +1273,19 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
449
1273
|
let names = hoistedVarNames.get(node);
|
|
450
1274
|
if (names === undefined) {
|
|
451
1275
|
names = new Map();
|
|
452
|
-
function collect(value) {
|
|
1276
|
+
function collect(value, root = true) {
|
|
453
1277
|
if (value == null || typeof value !== 'object') return;
|
|
454
1278
|
if (Array.isArray(value)) {
|
|
455
|
-
for (const child of value) collect(child);
|
|
1279
|
+
for (const child of value) collect(child, false);
|
|
456
1280
|
return;
|
|
457
1281
|
}
|
|
458
1282
|
if (
|
|
459
1283
|
FUNCTION_TYPES.has(value.type) ||
|
|
1284
|
+
(!root && value.type === 'JSXCodeBlock') ||
|
|
1285
|
+
value.type === 'JSXIfExpression' ||
|
|
1286
|
+
value.type === 'JSXForExpression' ||
|
|
1287
|
+
value.type === 'JSXSwitchExpression' ||
|
|
1288
|
+
value.type === 'JSXTryExpression' ||
|
|
460
1289
|
value.type === 'ClassDeclaration' ||
|
|
461
1290
|
value.type === 'ClassExpression' ||
|
|
462
1291
|
value.type === 'StaticBlock' ||
|
|
@@ -470,7 +1299,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
470
1299
|
}
|
|
471
1300
|
}
|
|
472
1301
|
for (const key in value) {
|
|
473
|
-
if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) collect(value[key]);
|
|
1302
|
+
if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) collect(value[key], false);
|
|
474
1303
|
}
|
|
475
1304
|
}
|
|
476
1305
|
collect(node);
|
|
@@ -2055,6 +2884,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2055
2884
|
return;
|
|
2056
2885
|
case 'BlockStatement':
|
|
2057
2886
|
case 'JSXCodeBlock': {
|
|
2887
|
+
if (node.type === 'JSXCodeBlock') hasNestedCodeBlock = true;
|
|
2058
2888
|
const block = createScope(scope, 'block', node.body ?? []);
|
|
2059
2889
|
const executionPhase = visitStatements(node.body, block, phase);
|
|
2060
2890
|
if (node.render) visit(node.render, block, executionPhase);
|
|
@@ -2494,6 +3324,9 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
2494
3324
|
}
|
|
2495
3325
|
|
|
2496
3326
|
for (const statement of ast.body ?? []) visit(statement, moduleScope, 'module');
|
|
3327
|
+
if (hasNestedCodeBlock) {
|
|
3328
|
+
diagnostics.push(...strongLocalityDiagnostics(ast, source, filename, isReassigned));
|
|
3329
|
+
}
|
|
2497
3330
|
return { enabled, diagnostics };
|
|
2498
3331
|
}
|
|
2499
3332
|
|