Files
v8/test/debugger/debug/debug-evaluate-shadowed-context.js
Simon Zünd e58e145c8c [debug] Walk outer lexical scopes via DebugScriptScope
Now that DebugScriptScopeInfo provides the full lexical scope tree for
every script, ScopeIterator no longer switches to context-only
iteration when it leaves the paused closure. Instead it keeps walking
DebugScriptScope into outer lexical scopes, including outer scopes
that don't need a context. Stack-allocated variables of such scopes
are reported as unavailable. Iteration falls back to the context chain
once it leaves the root of the scope tree (e.g. for eval and wrapped
functions).

Empty outer scopes are still skipped for now; a follow-up changes that.
[[Scopes]] is unaffected, since the inspector builds it from the runtime
context chain.

- GetFunctionDebugName() returns undefined for outer scopes whose
  enclosing function has no context, instead of using the name of an
  unrelated closure context.
- DebugEvaluate uses the closest runtime context when targeting an
  outer scope without a context (EvaluationContext()), and always wraps
  it for contextless target scopes so the scope's blocklist applies,
  even if the closest context is the native context. Otherwise
  evaluating `x` in a contextless scope that declares `x` would find a
  global `x`.

R=pfaffe@chromium.org

Bug: 545412300
Change-Id: Iaee6cfb7e1bee13c0bd6c3ff61d7e9c82a9b8491
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8460005
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Commit-Queue: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110376}
2026-10-06 21:52:25 -07:00

328 lines
10 KiB
JavaScript

// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --no-analyze-environment-liveness --no-experimental-reuse-locals-blocklists
// Test that debug-evaluate only resolves variables that are used by
// the function inside which we debug-evaluate. This is to avoid
// incorrect variable resolution when a context-allocated variable is
// shadowed by a stack-allocated variable.
Debug = debug.Debug
let test_name;
let listener_delegate;
let listener_called;
let exception;
let begin_test_count = 0;
let end_test_count = 0;
let break_count = 0;
// Debug event listener which delegates.
function listener(event, exec_state, event_data, data) {
try {
if (event == Debug.DebugEvent.Break) {
break_count++;
listener_called = true;
listener_delegate(exec_state);
}
} catch (e) {
exception = e;
print(e, e.stack);
}
}
Debug.setListener(listener);
function BeginTest(name) {
test_name = name;
listener_called = false;
exception = null;
begin_test_count++;
}
function EndTest() {
assertTrue(listener_called, "listener not called for " + test_name);
assertNull(exception, test_name + " / " + exception);
end_test_count++;
}
BeginTest("Check that 'x' resolves correctly and 'a' is written correctly");
var a = "a";
function f1() {
var x = 1; // context allocate x
(() => x);
var y = "y";
var z = "z";
(function () {
var x = 2; // stack allocate shadowing x
(function () {
y; // access y
debugger; // ReferenceError
})(); // 2
})(); // 1
return y;
}
listener_delegate = function(exec_state) {
for (var i = 0; i < exec_state.frameCount() - 1; i++) {
var frame = exec_state.frame(i);
var value;
try {
value = frame.evaluate("x").value();
} catch (e) {
value = e.name;
}
print(frame.sourceLineText());
var expected = frame.sourceLineText().match(/\/\/ (.*$)/)[1];
assertEquals(String(expected), String(value));
}
assertEquals("[object global]",
String(exec_state.frame(0).evaluate("this").value()));
assertEquals("y", exec_state.frame(0).evaluate("y").value());
assertEquals("a", exec_state.frame(0).evaluate("a").value());
exec_state.frame(0).evaluate("a = 'A'");
assertThrows(() => exec_state.frame(0).evaluate("z"), ReferenceError);
}
f1();
assertEquals("A", a);
a = "a";
EndTest();
BeginTest("Check that a context-allocated 'this' works")
function f2() {
var x = 1; // context allocate x
(() => x);
var y = "y";
var z = "z";
(function() {
var x = 2; // stack allocate shadowing x
(() => {
y;
a;
this; // context allocate receiver
debugger; // ReferenceError
})(); // 2
})(); // 1
return y;
};
// Uses the same listener delgate as for `f1`.
f2();
assertEquals("A", a);
EndTest();
BeginTest("Check that we don't get confused with nested scopes");
function f3() {
var x = 1; // context allocate x
(() => x);
(function() {
var x = 2; // stack allocate shadowing x
(function() {
{ // context allocate x in a nested scope
let x = 3;
(() => x);
}
debugger;
})();
})();
}
listener_delegate = function(exec_state) {
assertThrows(() => exec_state.frame(0).evaluate("x").value());
}
f3();
EndTest();
BeginTest("Check that stack-allocated variable is unavailable");
function f4() {
let a = 1;
let b = 2;
let c = 3;
() => a + c; // a and c are context-allocated
return function g() {
let a = 2; // a is stack-allocated
return function h() {
b; // b is allocated onto f's context.
debugger;
}
}
}
listener_delegate = function(exec_state) {
assertEquals(2, exec_state.frame(0).evaluate("b").value());
assertEquals(3, exec_state.frame(0).evaluate("c").value())
assertThrows(() => exec_state.frame(0).evaluate("a").value());
};
(f4())()();
EndTest();
BeginTest("Check that block lists on the closure boundary work as expected");
function f5() {
let a = 1;
() => a; // a is context-allocated
return function g() {
let a = 2; // a is stack-allocated
{
let b = 3;
return function h() {
debugger;
}
}
}
}
listener_delegate = function(exec_state) {
assertThrows(() => exec_state.frame(0).evaluate("a").value());
};
(f5())()();
EndTest();
BeginTest("Check evaluating on outer closure scopes with shadowing");
function test_scopes_shadowing() {
let x = 'outer_x';
let y = 'outer_y';
() => [x, y]; // context allocate x and y in f7
return function g() {
let x = 'middle_x';
() => x; // context allocate x in g
return function h() {
let x = 'inner_x_stack'; // stack allocate x in h
debugger;
};
};
}
listener_delegate = function(exec_state) {
assertEquals('inner_x_stack', exec_state.frame(0).scope(0).evaluate("x").value());
assertEquals('outer_y', exec_state.frame(0).scope(0).evaluate("y").value());
assertEquals('middle_x', exec_state.frame(0).scope(1).evaluate("x").value());
assertEquals('outer_y', exec_state.frame(0).scope(1).evaluate("y").value());
assertEquals('outer_x', exec_state.frame(0).scope(2).evaluate("x").value());
assertEquals('outer_y', exec_state.frame(0).scope(2).evaluate("y").value());
};
(test_scopes_shadowing())()();
EndTest();
BeginTest("Check that outer functions also get the correct block list calculated");
// This test is important once we reuse block list info. The block list for `g`
// needs to be correctly calculated already when we stop on break_position 1.
let break_position;
function f6() {
let a = 1; // stack-allocated
return function g() { // g itself doesn't require a context.
if (break_position === 2) debugger;
let a = 2; (() => a); // context-allocated
return function h() {
if (break_position === 1) debugger;
}
}
}
listener_delegate = function (exec_state) {
assertEquals(2, exec_state.frame(0).evaluate("a").value());
}
break_position = 1;
(f6())()();
EndTest();
BeginTest("Check that outer functions also get the correct block list calculated (continued)");
listener_delegate = function (exec_state) {
assertThrows(() => exec_state.frame(0).evaluate("a").value());
}
break_position = 2;
(f6())()();
EndTest();
BeginTest("Check evaluating on outer closure scope with stack-allocated shadowed variable");
var x = 'global_x';
function test_outer_shadow() {
let x = 'outer_stack';
let closure_y = 'closure_y';
() => closure_y; // force context allocation for closure_y
return function inner() {
debugger;
};
}
listener_delegate = function(exec_state) {
assertThrows(() => exec_state.frame(0).scope(1).evaluate("x").value(), ReferenceError);
assertEquals('closure_y', exec_state.frame(0).scope(1).evaluate("closure_y").value());
};
(test_outer_shadow())();
EndTest();
BeginTest("Check evaluating on outer closure scopes when inner scope also has a context");
function test_outer_shadow_with_inner_context() {
let x = 'outer_stack';
let closure_y = 'closure_y';
() => closure_y; // force context allocation for test_outer_shadow_with_inner_context
return function inner() {
let inner_ctx = 'inner_ctx';
() => inner_ctx; // force context allocation for inner as well
debugger;
};
}
listener_delegate = function(exec_state) {
assertThrows(() => exec_state.frame(0).scope(1).evaluate("x").value(), ReferenceError);
assertEquals('closure_y', exec_state.frame(0).scope(1).evaluate("closure_y").value());
};
(test_outer_shadow_with_inner_context())();
EndTest();
BeginTest("Check that inner block locals do not leak into outer function block list");
var a_block_test = 'global_a';
var c_block_test = 'global_c';
function test_inner_block_isolation() {
let a_block_test = 42; // stack-allocated in f
let b_block_test = 21;
() => b_block_test; // force context-allocation so f has a context
{
const c_block_test = 'foo'; // stack-allocated in inner block
(function g() {
if (break_position === 1) debugger;
})();
}
return function h() {
if (break_position === 2) debugger;
};
}
listener_delegate = function(exec_state) {
// In `g` (inside the inner block), both `a_block_test` and `c_block_test` are shadowed.
assertThrows(() => exec_state.frame(0).scope(0).evaluate("a_block_test").value(), ReferenceError);
assertThrows(() => exec_state.frame(0).scope(0).evaluate("c_block_test").value(), ReferenceError);
assertEquals(21, exec_state.frame(0).scope(0).evaluate("b_block_test").value());
// The inner block (`scope(1)`) has no context, but still shadows both.
assertThrows(() => exec_state.frame(0).scope(1).evaluate("a_block_test").value(), ReferenceError);
assertThrows(() => exec_state.frame(0).scope(1).evaluate("c_block_test").value(), ReferenceError);
assertEquals(21, exec_state.frame(0).scope(1).evaluate("b_block_test").value());
// In `f` (`scope(2)`), `a_block_test` is shadowed, but `c_block_test` (from the inner block) is not.
assertThrows(() => exec_state.frame(0).scope(2).evaluate("a_block_test").value(), ReferenceError);
assertEquals('global_c', exec_state.frame(0).scope(2).evaluate("c_block_test").value());
assertEquals(21, exec_state.frame(0).scope(2).evaluate("b_block_test").value());
};
break_position = 1;
(test_inner_block_isolation())();
EndTest();
BeginTest("Check that inner block locals do not leak into sibling closure after reuse");
listener_delegate = function(exec_state) {
// In `h` (outside the inner block), `a_block_test` is shadowed, but `c_block_test` is not.
assertThrows(() => exec_state.frame(0).scope(0).evaluate("a_block_test").value(), ReferenceError);
assertEquals('global_c', exec_state.frame(0).scope(0).evaluate("c_block_test").value());
assertEquals(21, exec_state.frame(0).scope(0).evaluate("b_block_test").value());
};
break_position = 2;
(test_inner_block_isolation())();
EndTest();
assertEquals(begin_test_count, break_count,
'one or more tests did not enter the debugger');
assertEquals(begin_test_count, end_test_count,
'one or more tests did not have its result checked');