[inspector] Fix mapping between location and offset.

We weren't really translating between location (line and column number)
and source position (character offset) consistently, especially when it
came to inline <script>s. There were also inconsistencies between what
Debugger.getPossibleBreakpoints and Debugger.setBreakpointByUrl would
do.

With this CL, we are now consistently operating under the following
assumptions:

(1) For inline <scripts>s with a //@ sourceURL annotation, we assume
    that the line and column number that comes in via the protocol is
    in terms of the source text of the script.
(2) For inline <script>s without said annotation, we assume that the
    line and column numbers are in terms of the surrounding document.

This is finally aligned with how the DevTools front-end operates.

Fixed: chromium:1319828
Change-Id: I98c4ef04b34a97caf060ff4f32690b135edb6ee6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3610622
Reviewed-by: Kim-Anh Tran <kimanh@chromium.org>
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Kim-Anh Tran <kimanh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80292}
This commit is contained in:
Benedikt Meurer 2022-04-29 13:07:01 +02:00 committed by V8 LUCI CQ
parent 1a80bfc1d5
commit d821a6a373
10 changed files with 191 additions and 75 deletions

View File

@ -535,16 +535,17 @@ bool Script::GetPossibleBreakpoints(
#endif // V8_ENABLE_WEBASSEMBLY
i::Isolate* isolate = script->GetIsolate();
i::Script::InitLineEnds(isolate, script);
CHECK(script->line_ends().IsFixedArray());
i::Handle<i::FixedArray> line_ends =
i::Handle<i::FixedArray>::cast(i::handle(script->line_ends(), isolate));
CHECK(line_ends->length());
int start_offset = GetSourceOffset(start);
int end_offset = end.IsEmpty()
? GetSmiValue(line_ends, line_ends->length() - 1) + 1
: GetSourceOffset(end);
int start_offset, end_offset;
if (!GetSourceOffset(start, GetSourceOffsetMode::kClamp).To(&start_offset)) {
return false;
}
if (end.IsEmpty()) {
end_offset = std::numeric_limits<int>::max();
} else if (!GetSourceOffset(end, GetSourceOffsetMode::kClamp)
.To(&end_offset)) {
return false;
}
if (start_offset >= end_offset) return true;
std::vector<i::BreakLocation> v8_locations;
@ -555,53 +556,72 @@ bool Script::GetPossibleBreakpoints(
}
std::sort(v8_locations.begin(), v8_locations.end(), CompareBreakLocation);
int current_line_end_index = 0;
for (const auto& v8_location : v8_locations) {
int offset = v8_location.position();
while (offset > GetSmiValue(line_ends, current_line_end_index)) {
++current_line_end_index;
CHECK(current_line_end_index < line_ends->length());
}
int line_offset = 0;
if (current_line_end_index > 0) {
line_offset = GetSmiValue(line_ends, current_line_end_index - 1) + 1;
}
locations->emplace_back(
current_line_end_index + script->line_offset(),
offset - line_offset +
(current_line_end_index == 0 ? script->column_offset() : 0),
v8_location.type());
Location location = GetSourceLocation(v8_location.position());
locations->emplace_back(location.GetLineNumber(),
location.GetColumnNumber(), v8_location.type());
}
return true;
}
int Script::GetSourceOffset(const Location& location) const {
Maybe<int> Script::GetSourceOffset(const Location& location,
GetSourceOffsetMode mode) const {
i::Handle<i::Script> script = Utils::OpenHandle(this);
#if V8_ENABLE_WEBASSEMBLY
if (script->type() == i::Script::TYPE_WASM) {
DCHECK_EQ(0, location.GetLineNumber());
return location.GetColumnNumber();
return Just(location.GetColumnNumber());
}
#endif // V8_ENABLE_WEBASSEMBLY
int line = std::max(location.GetLineNumber() - script->line_offset(), 0);
int line = location.GetLineNumber();
int column = location.GetColumnNumber();
if (line == 0) {
column = std::max(0, column - script->column_offset());
if (!script->HasSourceURLComment()) {
// Line/column number for inline <script>s with sourceURL annotation
// are supposed to be related to the <script> tag, otherwise they
// are relative to the parent file. Keep this in sync with the logic
// in GetSourceLocation() below.
line -= script->line_offset();
if (line == 0) column -= script->column_offset();
}
i::Script::InitLineEnds(script->GetIsolate(), script);
CHECK(script->line_ends().IsFixedArray());
i::Handle<i::FixedArray> line_ends = i::Handle<i::FixedArray>::cast(
i::handle(script->line_ends(), script->GetIsolate()));
CHECK(line_ends->length());
if (line >= line_ends->length())
return GetSmiValue(line_ends, line_ends->length() - 1);
int line_offset = GetSmiValue(line_ends, line);
if (line == 0) return std::min(column, line_offset);
int prev_line_offset = GetSmiValue(line_ends, line - 1);
return std::min(prev_line_offset + column + 1, line_offset);
if (line < 0) {
if (mode == GetSourceOffsetMode::kClamp) {
return Just(0);
}
return Nothing<int>();
}
if (line >= line_ends->length()) {
if (mode == GetSourceOffsetMode::kClamp) {
return Just(GetSmiValue(line_ends, line_ends->length() - 1));
}
return Nothing<int>();
}
if (column < 0) {
if (mode != GetSourceOffsetMode::kClamp) {
return Nothing<int>();
}
column = 0;
}
int offset = column;
if (line > 0) {
int prev_line_end_offset = GetSmiValue(line_ends, line - 1);
offset += prev_line_end_offset + 1;
}
int line_end_offset = GetSmiValue(line_ends, line);
if (offset > line_end_offset) {
// Be permissive with columns that don't exist,
// as long as they are clearly within the range
// of the script.
if (line < line_ends->length() - 1 || mode == GetSourceOffsetMode::kClamp) {
return Just(line_end_offset);
}
return Nothing<int>();
}
return Just(offset);
}
Location Script::GetSourceLocation(int offset) const {
@ -609,6 +629,10 @@ Location Script::GetSourceLocation(int offset) const {
i::Script::PositionInfo info;
i::Script::GetPositionInfo(script, offset, &info, i::Script::WITH_OFFSET);
if (script->HasSourceURLComment()) {
// Line/column number for inline <script>s with sourceURL annotation
// are supposed to be related to the <script> tag, otherwise they
// are relative to the parent file. Keep this in sync with the logic
// in GetSourceOffset() above.
info.line -= script->line_offset();
if (info.line == 0) info.column -= script->column_offset();
}
@ -627,7 +651,10 @@ bool Script::SetBreakpoint(Local<String> condition, Location* location,
BreakpointId* id) const {
i::Handle<i::Script> script = Utils::OpenHandle(this);
i::Isolate* isolate = script->GetIsolate();
int offset = GetSourceOffset(*location);
int offset;
if (!GetSourceOffset(*location).To(&offset)) {
return false;
}
if (!isolate->debug()->SetBreakPointForScript(
script, Utils::OpenHandle(*condition), &offset, id)) {
return false;

View File

@ -215,7 +215,10 @@ class V8_EXPORT_PRIVATE Script {
const debug::Location& start, const debug::Location& end,
bool restrict_to_function,
std::vector<debug::BreakLocation>* locations) const;
int GetSourceOffset(const debug::Location& location) const;
enum class GetSourceOffsetMode { kStrict, kClamp };
Maybe<int> GetSourceOffset(
const debug::Location& location,
GetSourceOffsetMode mode = GetSourceOffsetMode::kStrict) const;
v8::debug::Location GetSourceLocation(int offset) const;
bool SetScriptSource(v8::Local<v8::String> newSource, bool preview,
LiveEditResult* result) const;

View File

@ -180,8 +180,8 @@ bool positionComparator(const std::pair<int, int>& a,
String16 breakpointHint(const V8DebuggerScript& script, int lineNumber,
int columnNumber) {
int offset = script.offset(lineNumber, columnNumber);
if (offset == V8DebuggerScript::kNoOffset) return String16();
int offset;
if (!script.offset(lineNumber, columnNumber).To(&offset)) return String16();
String16 hint =
script.source(offset, kBreakpointHintMaxLength).stripWhiteSpace();
for (size_t i = 0; i < hint.length(); ++i) {
@ -206,8 +206,8 @@ void adjustBreakpointLocation(const V8DebuggerScript& script,
}
if (hint.isEmpty()) return;
intptr_t sourceOffset = script.offset(*lineNumber, *columnNumber);
if (sourceOffset == V8DebuggerScript::kNoOffset) return;
int sourceOffset;
if (!script.offset(*lineNumber, *columnNumber).To(&sourceOffset)) return;
intptr_t searchRegionOffset = std::max(
sourceOffset - kBreakpointHintMaxSearchOffset, static_cast<intptr_t>(0));
@ -948,16 +948,6 @@ V8DebuggerAgentImpl::setBreakpointImpl(const String16& breakpointId,
ScriptsMap::iterator scriptIterator = m_scripts.find(scriptId);
if (scriptIterator == m_scripts.end()) return nullptr;
V8DebuggerScript* script = scriptIterator->second.get();
if (lineNumber < script->startLine() || script->endLine() < lineNumber) {
return nullptr;
}
if (lineNumber == script->startLine() &&
columnNumber < script->startColumn()) {
return nullptr;
}
if (lineNumber == script->endLine() && script->endColumn() < columnNumber) {
return nullptr;
}
v8::debug::BreakpointId debuggerBreakpointId;
v8::debug::Location location(lineNumber, columnNumber);

View File

@ -231,7 +231,7 @@ class ActualScript : public V8DebuggerScript {
v8::debug::ResetBlackboxedStateCache(m_isolate, m_script.Get(m_isolate));
}
int offset(int lineNumber, int columnNumber) const override {
v8::Maybe<int> offset(int lineNumber, int columnNumber) const override {
v8::HandleScope scope(m_isolate);
return m_script.Get(m_isolate)->GetSourceOffset(
v8::debug::Location(lineNumber, columnNumber));

View File

@ -90,8 +90,7 @@ class V8DebuggerScript {
std::vector<v8::debug::BreakLocation>* locations) = 0;
virtual void resetBlackboxedStateCache() = 0;
static const int kNoOffset = -1;
virtual int offset(int lineNumber, int columnNumber) const = 0;
virtual v8::Maybe<int> offset(int lineNumber, int columnNumber) const = 0;
virtual v8::debug::Location location(int offset) const = 0;
virtual bool setBreakpoint(const String16& condition,

View File

@ -4776,26 +4776,40 @@ TEST(SourceInfo) {
CHECK_EQ(script->GetSourceLocation(start_d).GetColumnNumber(), 10);
// Test offsets.
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(1, 10)), start_a);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(2, 13)), start_b);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(3, 0)), start_b + 5);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(3, 2)), start_b + 7);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(4, 0)), start_b + 16);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(5, 12)), start_c);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(6, 0)), start_c + 6);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(7, 0)), start_c + 19);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(8, 0)), start_c + 35);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(9, 0)), start_c + 48);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(10, 0)), start_c + 64);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(11, 0)), start_c + 70);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(12, 10)), start_d);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(13, 0)), start_d + 6);
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(1, 10)),
v8::Just(start_a));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(2, 13)),
v8::Just(start_b));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(3, 0)),
v8::Just(start_b + 5));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(3, 2)),
v8::Just(start_b + 7));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(4, 0)),
v8::Just(start_b + 16));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(5, 12)),
v8::Just(start_c));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(6, 0)),
v8::Just(start_c + 6));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(7, 0)),
v8::Just(start_c + 19));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(8, 0)),
v8::Just(start_c + 35));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(9, 0)),
v8::Just(start_c + 48));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(10, 0)),
v8::Just(start_c + 64));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(11, 0)),
v8::Just(start_c + 70));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(12, 10)),
v8::Just(start_d));
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(13, 0)),
v8::Just(start_d + 6));
for (int i = 1; i <= num_lines_d; ++i) {
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(start_line_d + i, 0)),
6 + (i * line_length_d) + start_d);
v8::Just(6 + (i * line_length_d) + start_d));
}
CHECK_EQ(script->GetSourceOffset(v8::debug::Location(start_line_d + 17, 0)),
start_d + 158);
v8::Nothing<int>());
// Make sure invalid inputs work properly.
const int last_position = static_cast<int>(strlen(source)) - 1;

View File

@ -63,4 +63,4 @@ Adding breakpoint
evaluating foo1(0):
hit expected breakpoint
evaluating foo2(0):
not paused
hit expected breakpoint

View File

@ -93,7 +93,7 @@ function foo2(arg) {
}
//# sourceURL=test2.js`, 5);
await evaluate('foo1(0)', breakpointId);
await evaluate('foo2(0)');
await evaluate('foo2(0)', breakpointId);
}
]);

View File

@ -0,0 +1,18 @@
Regression test for crbug.com/1319828
Running test: testDebuggerGetPossibleBreakpoints
function foo() {
|_|console.|C|log('Hello World!');
|R|}
Running test: testDebuggerSetBreakpointByUrl
function foo() {
#console.log('Hello World!');
}
function foo() {
console.#log('Hello World!');
}

View File

@ -0,0 +1,65 @@
// Copyright 2022 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.
let {session, contextGroup, Protocol} =
InspectorTest.start('Regression test for crbug.com/1319828');
const source = `
function foo() {
console.log('Hello World!');
}
//# sourceURL=foo.js`;
contextGroup.addScript(source, 42, 3);
session.setupScriptMap();
InspectorTest.runAsyncTestSuite([
async function testDebuggerGetPossibleBreakpoints() {
const [{params: {scriptId}}] = await Promise.all([
Protocol.Debugger.onceScriptParsed(),
Protocol.Runtime.enable(),
Protocol.Debugger.enable(),
]);
const {result: {locations}} =
await Protocol.Debugger.getPossibleBreakpoints({
start: {lineNumber: 1, columnNumber: 0, scriptId},
end: {lineNumber: 3, columnNumber: 1, scriptId},
});
await session.logBreakLocations(locations);
await Promise.all([
Protocol.Debugger.disable(),
Protocol.Runtime.disable(),
]);
},
async function testDebuggerSetBreakpointByUrl() {
await Promise.all([
Protocol.Runtime.enable(),
Protocol.Debugger.enable(),
]);
let {result: {breakpointId, locations}} =
await Protocol.Debugger.setBreakpointByUrl({
url: 'foo.js',
lineNumber: 2,
columnNumber: 0,
});
await Promise.all([
session.logSourceLocations(locations),
Protocol.Debugger.removeBreakpoint({breakpointId}),
]);
({result: {breakpointId, locations}} =
await Protocol.Debugger.setBreakpointByUrl({
url: 'foo.js',
lineNumber: 2,
columnNumber: 9,
}));
await Promise.all([
session.logSourceLocations(locations),
Protocol.Debugger.removeBreakpoint({breakpointId}),
]);
await Promise.all([
Protocol.Debugger.disable(),
Protocol.Runtime.disable(),
]);
},
]);