diff --git a/API/hermes/DebuggerAPI.cpp b/API/hermes/DebuggerAPI.cpp index 665737a832e..b9a10588881 100644 --- a/API/hermes/DebuggerAPI.cpp +++ b/API/hermes/DebuggerAPI.cpp @@ -152,6 +152,16 @@ std::vector Debugger::getBreakpoints() { return impl_->getBreakpoints(); } +std::vector Debugger::getPossibleBreakpoints( + ScriptID scriptId, + uint32_t startLine, + uint32_t startColumn, + uint32_t endLine, + uint32_t endColumn) { + return impl_->getPossibleBreakpoints( + scriptId, startLine, startColumn, endLine, endColumn); +} + void Debugger::setShouldPauseOnScriptLoad(bool flag) { return impl_->setShouldPauseOnScriptLoad(flag); } diff --git a/API/hermes/DebuggerAPI.h b/API/hermes/DebuggerAPI.h index fe11b0cfac4..85c015be7fa 100644 --- a/API/hermes/DebuggerAPI.h +++ b/API/hermes/DebuggerAPI.h @@ -254,6 +254,20 @@ class HERMES_EXPORT Debugger { /// \return a list of extant breakpoints. std::vector getBreakpoints(); + /// \return the possible breakpoint locations within the source range + /// [\p startLine:\p startColumn, \p endLine:\p endColumn) of the script + /// identified by \p scriptId. Lines and columns are 1-based. A \p startColumn + /// of 1 includes the whole start line. If \p endLine is kInvalidLocation, the + /// range extends to the end of the script; otherwise \p endColumn is the + /// exclusive end column on \p endLine. Returns an empty vector if the script + /// is unknown or has no debug info. + std::vector getPossibleBreakpoints( + ScriptID scriptId, + uint32_t startLine, + uint32_t startColumn, + uint32_t endLine, + uint32_t endColumn); + /// Set whether the debugger should pause when an exception is thrown. void setPauseOnThrowMode(PauseOnThrowMode mode); @@ -465,6 +479,14 @@ class Debugger { std::vector getBreakpoints() { return std::vector(); } + std::vector getPossibleBreakpoints( + ScriptID scriptId, + uint32_t startLine, + uint32_t startColumn, + uint32_t endLine, + uint32_t endColumn) { + return {}; + } void setPauseOnThrowMode(PauseOnThrowMode mode) {} PauseOnThrowMode getPauseOnThrowMode() const { return PauseOnThrowMode::None; diff --git a/API/hermes/cdp/CDPAgent.cpp b/API/hermes/cdp/CDPAgent.cpp index 9472a03ceb6..664be701971 100644 --- a/API/hermes/cdp/CDPAgent.cpp +++ b/API/hermes/cdp/CDPAgent.cpp @@ -428,6 +428,9 @@ void CDPAgentImpl::DomainAgentsImpl::handleCommand( } else if (method == "setBreakpointByUrl") { debuggerAgent_->setBreakpointByUrl( static_cast(*command)); + } else if (method == "getPossibleBreakpoints") { + debuggerAgent_->getPossibleBreakpoints( + static_cast(*command)); } else if (method == "removeBreakpoint") { debuggerAgent_->removeBreakpoint( static_cast(*command)); diff --git a/API/hermes/cdp/DebuggerDomainAgent.cpp b/API/hermes/cdp/DebuggerDomainAgent.cpp index 825bc905bd7..84fc6791fad 100644 --- a/API/hermes/cdp/DebuggerDomainAgent.cpp +++ b/API/hermes/cdp/DebuggerDomainAgent.cpp @@ -497,6 +497,117 @@ void DebuggerDomainAgent::setBreakpointByUrl( sendResponseToClient(resp); } +void DebuggerDomainAgent::getPossibleBreakpoints( + const m::debugger::GetPossibleBreakpointsRequest &req) { + if (!checkDebuggerEnabled(req)) { + return; + } + + // The start and end locations must refer to the same script. + if (req.end.has_value() && req.end->scriptId != req.start.scriptId) { + sendResponseToClient( + m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "Invalid range")); + return; + } + + // The start must not be positioned after the end. + if (req.end.has_value()) { + long long startColumn = req.start.columnNumber.value_or(0); + long long endColumn = req.end->columnNumber.value_or(0); + if (req.start.lineNumber > req.end->lineNumber || + (req.start.lineNumber == req.end->lineNumber && + startColumn > endColumn)) { + sendResponseToClient( + m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "Invalid range")); + return; + } + } + + // Parse and validate the script ID. Reject anything that isn't a plain + // number to avoid throwing in std::stoull (the digit-count guard keeps the + // value within uint64_t range). + const std::string &scriptIdStr = req.start.scriptId; + if (scriptIdStr.empty() || scriptIdStr.size() > 19 || + scriptIdStr.find_first_not_of("0123456789") != std::string::npos) { + sendResponseToClient( + m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "No script with id")); + return; + } + auto scriptID = static_cast(std::stoull(scriptIdStr)); + + bool scriptFound = false; + for (const auto &srcLoc : runtime_.getDebugger().getLoadedScripts()) { + if (srcLoc.fileId == scriptID) { + scriptFound = true; + break; + } + } + if (!scriptFound) { + sendResponseToClient( + m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "No script with id")); + return; + } + + // Convert the CDP 0-based range to Hermes' 1-based, half-open range. + uint32_t startLine = static_cast(req.start.lineNumber) + 1; + uint32_t startColumn = req.start.columnNumber.has_value() + ? static_cast(req.start.columnNumber.value()) + 1 + : 1; + uint32_t endLine = debugger::kInvalidLocation; + uint32_t endColumn = debugger::kInvalidLocation; + if (req.end.has_value()) { + endLine = static_cast(req.end->lineNumber) + 1; + // endColumn is an exclusive bound; an omitted column means the whole end + // line, not column 1. + endColumn = req.end->columnNumber.has_value() + ? static_cast(req.end->columnNumber.value()) + 1 + : debugger::kInvalidLocation; + } + + std::vector breakLocations = + runtime_.getDebugger().getPossibleBreakpoints( + scriptID, startLine, startColumn, endLine, endColumn); + + // Match V8's cap on the number of returned locations to bound the response + // size (see kMaxNumBreakpoints in V8's v8-debugger-agent-impl.cc). With + // statement-level granularity this is rarely hit. + constexpr size_t kMaxNumBreakLocations = 1000; + size_t count = breakLocations.size(); + if (count > kMaxNumBreakLocations) { + count = kMaxNumBreakLocations; + } + + m::debugger::GetPossibleBreakpointsResponse resp; + resp.id = req.id; + resp.locations.reserve(count); + for (size_t i = 0; i < count; ++i) { + const debugger::BreakLocation &breakLocation = breakLocations[i]; + m::debugger::BreakLocation cdpLocation; + cdpLocation.scriptId = std::to_string(breakLocation.location.fileId); + m::setChromeLocation(cdpLocation, breakLocation.location); + switch (breakLocation.type) { + case debugger::BreakLocationType::DebuggerStatement: + cdpLocation.type = "debuggerStatement"; + break; + case debugger::BreakLocationType::Call: + cdpLocation.type = "call"; + break; + case debugger::BreakLocationType::Return: + cdpLocation.type = "return"; + break; + case debugger::BreakLocationType::None: + break; + } + resp.locations.push_back(std::move(cdpLocation)); + } + + sendResponseToClient(resp); +} + void DebuggerDomainAgent::removeBreakpoint( const m::debugger::RemoveBreakpointRequest &req) { if (!checkDebuggerEnabled(req)) { diff --git a/API/hermes/cdp/DebuggerDomainAgent.h b/API/hermes/cdp/DebuggerDomainAgent.h index 4c85418eeaa..5e83856dd99 100644 --- a/API/hermes/cdp/DebuggerDomainAgent.h +++ b/API/hermes/cdp/DebuggerDomainAgent.h @@ -143,6 +143,12 @@ class DebuggerDomainAgent : public DomainAgent { /// even in the same CDP session. For comparison, V8 allows it from different /// sessions but disallows it within a single session. void setBreakpointByUrl(const m::debugger::SetBreakpointByUrlRequest &req); + /// @cdp Debugger.getPossibleBreakpoints returns the valid breakpoint + /// locations within a source range. The `start` and (if provided) `end` + /// locations must refer to the same script. `restrictToFunction` is not yet + /// supported and is ignored. + void getPossibleBreakpoints( + const m::debugger::GetPossibleBreakpointsRequest &req); /// Handles Debugger.removeBreakpoint void removeBreakpoint(const m::debugger::RemoveBreakpointRequest &req); /// Handles Debugger.setBreakpointsActive diff --git a/API/hermes/cdp/MessageTypes.cpp b/API/hermes/cdp/MessageTypes.cpp index e4820ffeee8..fba5490efe5 100644 --- a/API/hermes/cdp/MessageTypes.cpp +++ b/API/hermes/cdp/MessageTypes.cpp @@ -1,5 +1,5 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. -// @generated SignedSource<> +// @generated SignedSource<<330a0049c8eb8453a49b0b402e63aa47>> #include "MessageTypes.h" @@ -94,6 +94,8 @@ std::unique_ptr Request::fromJson(const std::string &str) { {"Debugger.enable", tryMake}, {"Debugger.evaluateOnCallFrame", tryMake}, + {"Debugger.getPossibleBreakpoints", + tryMake}, {"Debugger.pause", tryMake}, {"Debugger.removeBreakpoint", tryMake}, {"Debugger.resume", tryMake}, @@ -375,6 +377,27 @@ JSONValue *runtime::ExceptionDetails::toJsonVal(JSONFactory &factory) const { return factory.newObject(props.begin(), props.end()); } +std::unique_ptr debugger::BreakLocation::tryMake( + const JSONObject *obj) { + std::unique_ptr type = + std::make_unique(); + TRY_ASSIGN(type->scriptId, obj, "scriptId"); + TRY_ASSIGN(type->lineNumber, obj, "lineNumber"); + TRY_ASSIGN(type->columnNumber, obj, "columnNumber"); + TRY_ASSIGN(type->type, obj, "type"); + return type; +} + +JSONValue *debugger::BreakLocation::toJsonVal(JSONFactory &factory) const { + llvh::SmallVector props; + + put(props, "scriptId", scriptId, factory); + put(props, "lineNumber", lineNumber, factory); + put(props, "columnNumber", columnNumber, factory); + put(props, "type", type, factory); + return factory.newObject(props.begin(), props.end()); +} + std::unique_ptr debugger::Scope::tryMake( const JSONObject *obj) { std::unique_ptr type = std::make_unique(); @@ -786,6 +809,53 @@ void debugger::EvaluateOnCallFrameRequest::accept( handler.handle(*this); } +debugger::GetPossibleBreakpointsRequest::GetPossibleBreakpointsRequest() + : Request("Debugger.getPossibleBreakpoints") {} + +std::unique_ptr +debugger::GetPossibleBreakpointsRequest::tryMake(const JSONObject *obj) { + std::unique_ptr req = + std::make_unique(); + TRY_ASSIGN(req->id, obj, "id"); + TRY_ASSIGN(req->method, obj, "method"); + + JSONValue *v = obj->get("params"); + if (v == nullptr) { + return nullptr; + } + auto convertResult = valueFromJson(v); + if (!convertResult) { + return nullptr; + } + auto *params = *convertResult; + TRY_ASSIGN(req->start, params, "start"); + TRY_ASSIGN(req->end, params, "end"); + TRY_ASSIGN(req->restrictToFunction, params, "restrictToFunction"); + return req; +} + +JSONValue *debugger::GetPossibleBreakpointsRequest::toJsonVal( + JSONFactory &factory) const { + llvh::SmallVector paramsProps; + put(paramsProps, "start", start, factory); + put(paramsProps, "end", end, factory); + put(paramsProps, "restrictToFunction", restrictToFunction, factory); + + llvh::SmallVector props; + put(props, "id", id, factory); + put(props, "method", method, factory); + put(props, + "params", + factory.newObject(paramsProps.begin(), paramsProps.end()), + factory); + return factory.newObject(props.begin(), props.end()); +} + +void debugger::GetPossibleBreakpointsRequest::accept( + RequestHandler &handler) const { + handler.handle(*this); +} + debugger::PauseRequest::PauseRequest() : Request("Debugger.pause") {} std::unique_ptr debugger::PauseRequest::tryMake( @@ -2210,6 +2280,39 @@ JSONValue *debugger::EvaluateOnCallFrameResponse::toJsonVal( return factory.newObject(props.begin(), props.end()); } +std::unique_ptr +debugger::GetPossibleBreakpointsResponse::tryMake(const JSONObject *obj) { + std::unique_ptr resp = + std::make_unique(); + TRY_ASSIGN(resp->id, obj, "id"); + + JSONValue *v = obj->get("result"); + if (v == nullptr) { + return nullptr; + } + auto convertResult = valueFromJson(v); + if (!convertResult) { + return nullptr; + } + auto *res = *convertResult; + TRY_ASSIGN(resp->locations, res, "locations"); + return resp; +} + +JSONValue *debugger::GetPossibleBreakpointsResponse::toJsonVal( + JSONFactory &factory) const { + llvh::SmallVector resProps; + put(resProps, "locations", locations, factory); + + llvh::SmallVector props; + put(props, "id", id, factory); + put(props, + "result", + factory.newObject(resProps.begin(), resProps.end()), + factory); + return factory.newObject(props.begin(), props.end()); +} + std::unique_ptr debugger::SetBreakpointResponse::tryMake(const JSONObject *obj) { std::unique_ptr resp = diff --git a/API/hermes/cdp/MessageTypes.h b/API/hermes/cdp/MessageTypes.h index bdc14d394a7..682d94cb931 100644 --- a/API/hermes/cdp/MessageTypes.h +++ b/API/hermes/cdp/MessageTypes.h @@ -1,5 +1,5 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. -// @generated SignedSource<<1284c402aedd087ebdf70e9e76596f1c>> +// @generated SignedSource<<727dcfe67419625e56b2acb525424108>> #pragma once @@ -20,6 +20,7 @@ using JSONBlob = std::string; struct UnknownRequest; namespace debugger { +struct BreakLocation; using BreakpointId = std::string; struct BreakpointResolvedNotification; struct CallFrame; @@ -28,6 +29,8 @@ struct DisableRequest; struct EnableRequest; struct EvaluateOnCallFrameRequest; struct EvaluateOnCallFrameResponse; +struct GetPossibleBreakpointsRequest; +struct GetPossibleBreakpointsResponse; struct Location; struct PauseRequest; struct PausedNotification; @@ -133,6 +136,7 @@ struct RequestHandler { virtual void handle(const debugger::DisableRequest &req) = 0; virtual void handle(const debugger::EnableRequest &req) = 0; virtual void handle(const debugger::EvaluateOnCallFrameRequest &req) = 0; + virtual void handle(const debugger::GetPossibleBreakpointsRequest &req) = 0; virtual void handle(const debugger::PauseRequest &req) = 0; virtual void handle(const debugger::RemoveBreakpointRequest &req) = 0; virtual void handle(const debugger::ResumeRequest &req) = 0; @@ -180,6 +184,7 @@ struct NoopRequestHandler : public RequestHandler { void handle(const debugger::DisableRequest &req) override {} void handle(const debugger::EnableRequest &req) override {} void handle(const debugger::EvaluateOnCallFrameRequest &req) override {} + void handle(const debugger::GetPossibleBreakpointsRequest &req) override {} void handle(const debugger::PauseRequest &req) override {} void handle(const debugger::RemoveBreakpointRequest &req) override {} void handle(const debugger::ResumeRequest &req) override {} @@ -374,6 +379,21 @@ struct runtime::ExceptionDetails : public Serializable { std::optional executionContextId; }; +struct debugger::BreakLocation : public Serializable { + BreakLocation() = default; + BreakLocation(BreakLocation &&) = default; + BreakLocation(const BreakLocation &) = delete; + static std::unique_ptr tryMake(const JSONObject *obj); + JSONValue *toJsonVal(JSONFactory &factory) const override; + BreakLocation &operator=(const BreakLocation &) = delete; + BreakLocation &operator=(BreakLocation &&) = default; + + runtime::ScriptId scriptId{}; + long long lineNumber{}; + std::optional columnNumber; + std::optional type; +}; + struct debugger::Scope : public Serializable { Scope() = default; Scope(Scope &&) = default; @@ -627,6 +647,19 @@ struct debugger::EvaluateOnCallFrameRequest : public Request { std::optional throwOnSideEffect; }; +struct debugger::GetPossibleBreakpointsRequest : public Request { + GetPossibleBreakpointsRequest(); + static std::unique_ptr tryMake( + const JSONObject *obj); + + JSONValue *toJsonVal(JSONFactory &factory) const override; + void accept(RequestHandler &handler) const override; + + debugger::Location start{}; + std::optional end; + std::optional restrictToFunction; +}; + struct debugger::PauseRequest : public Request { PauseRequest(); static std::unique_ptr tryMake(const JSONObject *obj); @@ -1031,6 +1064,15 @@ struct debugger::EvaluateOnCallFrameResponse : public Response { std::optional exceptionDetails; }; +struct debugger::GetPossibleBreakpointsResponse : public Response { + GetPossibleBreakpointsResponse() = default; + static std::unique_ptr tryMake( + const JSONObject *obj); + JSONValue *toJsonVal(JSONFactory &factory) const override; + + std::vector locations; +}; + struct debugger::SetBreakpointResponse : public Response { SetBreakpointResponse() = default; static std::unique_ptr tryMake(const JSONObject *obj); diff --git a/API/hermes/cdp/tools/message_types.txt b/API/hermes/cdp/tools/message_types.txt index 5369b052d57..bc2c5143569 100644 --- a/API/hermes/cdp/tools/message_types.txt +++ b/API/hermes/cdp/tools/message_types.txt @@ -2,6 +2,7 @@ Debugger.breakpointResolved Debugger.disable Debugger.enable Debugger.evaluateOnCallFrame +Debugger.getPossibleBreakpoints Debugger.pause Debugger.paused Debugger.removeBreakpoint diff --git a/include/hermes/BCGen/HBC/DebugInfo.h b/include/hermes/BCGen/HBC/DebugInfo.h index 579667fa38b..ebaf2f18be0 100644 --- a/include/hermes/BCGen/HBC/DebugInfo.h +++ b/include/hermes/BCGen/HBC/DebugInfo.h @@ -191,6 +191,10 @@ struct DebugSearchResult { /// The actual column that the search found. uint32_t column{0}; + /// The statement at the found location. 1-based per function; 0 if the + /// location is not part of any user-written statement. + uint32_t statement{0}; + DebugSearchResult() {} DebugSearchResult( @@ -202,6 +206,18 @@ struct DebugSearchResult { bytecodeOffset(bytecodeOffset), line(line), column(column) {} + + DebugSearchResult( + uint32_t functionIndex, + uint32_t bytecodeOffset, + uint32_t line, + uint32_t column, + uint32_t statement) + : functionIndex(functionIndex), + bytecodeOffset(bytecodeOffset), + line(line), + column(column), + statement(statement) {} }; /// A data structure for storing debug info. @@ -316,6 +332,20 @@ class DebugInfo { uint32_t targetLine, OptValue targetColumn) const; + /// Find every distinct source location listed in the debug info for file + /// \p filenameId that falls within the half-open range + /// [\p startLine:\p startColumn, \p endLine:\p endColumn). Lines and columns + /// are 1-based. If \p endLine is None, the range extends to the end of the + /// file; \p endColumn is the exclusive end column on \p endLine and is + /// ignored when \p endLine is None. \return the matching locations, in + /// increasing bytecode order within each function. + std::vector getAllLocationsForRange( + uint32_t filenameId, + uint32_t startLine, + uint32_t startColumn, + OptValue endLine, + uint32_t endColumn) const; + private: /// \return the slice of data_ reflecting the source locations. llvh::ArrayRef sourceLocationsData() const { diff --git a/include/hermes/VM/Debugger/Debugger.h b/include/hermes/VM/Debugger/Debugger.h index b9158ddc9dc..4064588bb6c 100644 --- a/include/hermes/VM/Debugger/Debugger.h +++ b/include/hermes/VM/Debugger/Debugger.h @@ -71,6 +71,7 @@ class Debugger { using LexicalInfo = ::facebook::hermes::debugger::LexicalInfo; using ScriptID = ::facebook::hermes::debugger::ScriptID; using AsyncPauseKind = ::facebook::hermes::debugger::AsyncPauseKind; + using BreakLocation = ::facebook::hermes::debugger::BreakLocation; Runtime &runtime_; @@ -426,6 +427,19 @@ class Debugger { /// \return list of loaded scripts that haven't been garbage collected std::vector getLoadedScripts() const; + /// \return the possible breakpoint locations within the source range + /// [\p startLine:\p startColumn, \p endLine:\p endColumn) of the script + /// identified by \p scriptId. Lines and columns are 1-based. If \p endLine is + /// kInvalidLocation, the range extends to the end of the script. Only + /// already-compiled functions are considered; lazily-compiled functions have + /// no debug info yet and are not enumerated. + std::vector getPossibleBreakpoints( + ScriptID scriptId, + uint32_t startLine, + uint32_t startColumn, + uint32_t endLine, + uint32_t endColumn) const; + /// Find the handler for an exception thrown at \p state. /// \return llvh::None if no handler is found, else return the state of the /// handler and the offset of its frame. diff --git a/lib/BCGen/HBC/DebugInfo.cpp b/lib/BCGen/HBC/DebugInfo.cpp index 3c70858c7f5..9902402df1d 100644 --- a/lib/BCGen/HBC/DebugInfo.cpp +++ b/lib/BCGen/HBC/DebugInfo.cpp @@ -134,6 +134,71 @@ OptValue DebugInfo::getAddressForLocation( return best; } +std::vector DebugInfo::getAllLocationsForRange( + uint32_t filenameId, + uint32_t startLine, + uint32_t startColumn, + OptValue endLine, + uint32_t endColumn) const { + std::vector results; + + // Find the [start, end) debug-offset window covering the requested file, the + // same way getAddressForLocation does. + uint32_t start = 0; + uint32_t end = 0; + bool foundFile = false; + for (const auto &cur : files_) { + if (foundFile) { + end = cur.fromAddress; + break; + } + if (cur.filenameId == filenameId) { + foundFile = true; + start = cur.fromAddress; + end = sourceLocationsData().size(); + } + } + if (!foundFile) { + return results; + } + + // A location is in range if it is at or after the (startLine, startColumn) + // lower bound and, when an upper bound is provided, strictly before the + // (endLine, endColumn) exclusive upper bound. + auto inRange = [&](uint32_t line, uint32_t column) -> bool { + if (line < startLine || (line == startLine && column < startColumn)) { + return false; + } + if (endLine.hasValue() && + (line > *endLine || (line == *endLine && column >= endColumn))) { + return false; + } + return true; + }; + + uint32_t offset = start; + while (offset < end) { + FunctionDebugInfoDeserializer fdid(data_.getData(), offset); + while (!fdid.isDone()) { + auto loc = fdid.next(); + if (!loc.hasValue()) { + continue; + } + if (inRange(loc->line, loc->column)) { + results.emplace_back( + fdid.getFunctionIndex(), + loc->address, + loc->line, + loc->column, + loc->statement); + } + } + offset = fdid.getOffset(); + } + + return results; +} + void DebugInfo::disassembleFilenames(llvh::raw_ostream &os) const { os << "Debug filename table:\n"; for (uint32_t i = 0, e = getFilenameTable().size(); i < e; ++i) { diff --git a/lib/VM/Debugger/Debugger.cpp b/lib/VM/Debugger/Debugger.cpp index 58cc688ae86..7de5a658f60 100644 --- a/lib/VM/Debugger/Debugger.cpp +++ b/lib/VM/Debugger/Debugger.cpp @@ -22,6 +22,7 @@ #include "hermes/VM/RuntimeModule.h" #include "hermes/VM/StackFrame-inline.h" #include "hermes/VM/StringView.h" +#include "llvh/ADT/DenseMap.h" #ifdef HERMES_ENABLE_DEBUGGER @@ -1643,6 +1644,118 @@ auto Debugger::getLoadedScripts() const -> std::vector { return loadedScripts; } +auto Debugger::getPossibleBreakpoints( + ScriptID scriptId, + uint32_t startLine, + uint32_t startColumn, + uint32_t endLine, + uint32_t endColumn) const -> std::vector { + using fhd::kInvalidLocation; + std::vector result; + + OptValue endLineOpt = endLine == kInvalidLocation + ? OptValue{llvh::None} + : OptValue{endLine}; + + for (auto &runtimeModule : runtime_.getRuntimeModules()) { + const auto *debugInfo = runtimeModule.getBytecode()->getDebugInfo(); + if (!debugInfo) { + continue; + } + + for (const auto &file : debugInfo->viewFiles()) { + if (resolveScriptId(&runtimeModule, file.filenameId) != scriptId) { + continue; + } + + std::vector searchResults = + debugInfo->getAllLocationsForRange( + file.filenameId, startLine, startColumn, endLineOpt, endColumn); + std::string fileName = debugInfo->getUTF8FilenameByID(file.filenameId); + + // Reduce the per-instruction locations to roughly one per source + // statement, plus every call/return/debugger location. This matches the + // granularity of V8's getPossibleBreakpoints and of Hermes' own stepper, + // which stops at the first instruction of each distinct statement (see + // sameStatementDifferentInstruction and the statement == 0 checks in the + // step loop). Without this, the debug info's one-entry-per-instruction + // granularity surfaces a breakpoint at nearly every token. + // seenStatements tracks the (functionIndex, statement) pairs whose lead + // location has been emitted; seenIndex then de-duplicates by source + // position, keeping the most specific type. + llvh::DenseSet seenStatements; + llvh::DenseMap seenIndex; + for (const auto &searchResult : searchResults) { + fhd::BreakLocationType type = fhd::BreakLocationType::None; + CodeBlock *codeBlock = + runtimeModule.getCodeBlockMayAllocate(searchResult.functionIndex); + if (codeBlock != nullptr && + searchResult.bytecodeOffset < codeBlock->getOpcodeArray().size()) { + // Use getRealOpCode so an installed breakpoint doesn't masquerade as + // a debugger statement. + OpCode opCode = getRealOpCode(codeBlock, searchResult.bytecodeOffset); + if (opCode == OpCode::Debugger) { + type = fhd::BreakLocationType::DebuggerStatement; + } else if (isCallType(opCode)) { + type = fhd::BreakLocationType::Call; + } else if (opCode == OpCode::Ret) { + type = fhd::BreakLocationType::Return; + } + } + + // Keep the first location of each statement (the statement boundary) + // and any call/return/debugger location; skip other intra-statement + // instructions. + uint64_t statementKey = + (static_cast(searchResult.functionIndex) << 32) | + searchResult.statement; + bool isStatementLead = searchResult.statement != 0 && + seenStatements.insert(statementKey).second; + + uint64_t key = (static_cast(searchResult.line) << 32) | + searchResult.column; + auto existing = seenIndex.find(key); + bool seenPos = existing != seenIndex.end(); + + // Drop the compiler's implicit epilogue return: a Return that does not + // begin a statement (it reuses the last statement's index) and lands on + // a brand-new source position (the function's end). Explicit returns + // are statement leads, and an explicit return's terminator shares its + // statement's position, so neither is affected. + if (type == fhd::BreakLocationType::Return && !isStatementLead && + !seenPos) { + continue; + } + + if (!isStatementLead && type == fhd::BreakLocationType::None) { + continue; + } + + if (seenPos) { + BreakLocation &prev = result[existing->second]; + if (prev.type == fhd::BreakLocationType::None) { + prev.type = type; + } + continue; + } + + SourceLocation loc; + loc.line = searchResult.line; + loc.column = searchResult.column; + loc.fileId = scriptId; + loc.fileName = fileName; + + seenIndex[key] = result.size(); + result.push_back(BreakLocation{std::move(loc), type}); + } + + return result; + } + } + + return result; +} + auto Debugger::resolveScriptId( RuntimeModule *runtimeModule, uint32_t filenameId) const -> ScriptID { diff --git a/public/hermes/Public/DebuggerTypes.h b/public/hermes/Public/DebuggerTypes.h index a549e81d757..8996805ebb3 100644 --- a/public/hermes/Public/DebuggerTypes.h +++ b/public/hermes/Public/DebuggerTypes.h @@ -56,6 +56,29 @@ struct SourceLocation { String fileName; }; +/// The kind of program point at a possible breakpoint location. Mirrors the +/// `type` field of CDP's Debugger.BreakLocation. +enum class BreakLocationType { + /// No notable type. + None, + /// A `debugger;` statement. + DebuggerStatement, + /// A function call. + Call, + /// A return from a function. + Return, +}; + +/// A possible breakpoint location within a source range, as returned by +/// Debugger::getPossibleBreakpoints. +struct BreakLocation { + /// The resolved source location of the possible breakpoint. + SourceLocation location; + + /// The kind of program point at this location, if notable. + BreakLocationType type = BreakLocationType::None; +}; + /// CallFrameInfo is a value type representing an entry in a call stack. struct CallFrameInfo { /// Name of the function executing in this frame. diff --git a/unittests/API/CDPAgentTest.cpp b/unittests/API/CDPAgentTest.cpp index 9b7ab75dbe9..d5a03c53b95 100644 --- a/unittests/API/CDPAgentTest.cpp +++ b/unittests/API/CDPAgentTest.cpp @@ -2374,6 +2374,172 @@ TEST_F(CDPAgentTest, DebuggerSetBreakpointById) { ensureNotification(waitForMessage(), "Debugger.resumed"); } +TEST_F(CDPAgentTest, DebuggerGetPossibleBreakpoints) { + int msgId = 1; + + sendAndCheckResponse("Debugger.enable", msgId++); + + scheduleScript(R"( + debugger; // line 1 (CDP): pause + debuggerStatement + var a = 1; // line 2 (CDP) + Math.random(); // line 3 (CDP): call + )"); + + auto note = expectNotification("Debugger.scriptParsed"); + auto scriptID = jsonScope_.getString(note, {"params", "scriptId"}); + + // Execution pauses on the debugger statement; query while paused. + ensurePaused(waitForMessage(), "other", {{"global", 1, 1}}); + + // Request all breakpoint locations from the start of the script onward. + sendRequest( + "Debugger.getPossibleBreakpoints", + msgId, + [scriptID](::hermes::JSONEmitter &json) { + json.emitKey("start"); + json.openDict(); + json.emitKeyValue("scriptId", scriptID); + json.emitKeyValue("lineNumber", 0); + json.emitKeyValue("columnNumber", 0); + json.closeDict(); + }); + + auto resp = ensureGetPossibleBreakpointsResponse(waitForMessage(), msgId++); + EXPECT_FALSE(resp.locations.empty()); + + bool foundDebuggerStatement = false; + bool foundCall = false; + for (const m::debugger::BreakLocation &location : resp.locations) { + // Every location belongs to the queried script and carries a column. + EXPECT_EQ(location.scriptId, scriptID); + EXPECT_TRUE(location.columnNumber.has_value()); + if (location.type.has_value()) { + if (location.type.value() == "debuggerStatement") { + foundDebuggerStatement = true; + // The debugger statement is on the first line (0-based). + EXPECT_EQ(location.lineNumber, 1); + } else if (location.type.value() == "call") { + foundCall = true; + } + } + } + EXPECT_TRUE(foundDebuggerStatement); + EXPECT_TRUE(foundCall); + + sendAndCheckResponse("Debugger.resume", msgId++); + ensureNotification(waitForMessage(), "Debugger.resumed"); +} + +TEST_F(CDPAgentTest, DebuggerGetPossibleBreakpointsUnknownScript) { + int msgId = 1; + + sendAndCheckResponse("Debugger.enable", msgId++); + + // Query a script ID that was never loaded. + sendRequest( + "Debugger.getPossibleBreakpoints", + msgId, + [](::hermes::JSONEmitter &json) { + json.emitKey("start"); + json.openDict(); + json.emitKeyValue("scriptId", "12345"); + json.emitKeyValue("lineNumber", 0); + json.emitKeyValue("columnNumber", 0); + json.closeDict(); + }); + expectErrorMessageContaining("No script with id", msgId++); +} + +TEST_F(CDPAgentTest, DebuggerGetPossibleBreakpointsEmptyRange) { + int msgId = 1; + + sendAndCheckResponse("Debugger.enable", msgId++); + + scheduleScript(R"( + debugger; + var a = 1; + )"); + + auto note = expectNotification("Debugger.scriptParsed"); + auto scriptID = jsonScope_.getString(note, {"params", "scriptId"}); + + ensurePaused(waitForMessage(), "other", {{"global", 1, 1}}); + + // An empty range (start == end) yields no locations, but is not an error. + sendRequest( + "Debugger.getPossibleBreakpoints", + msgId, + [scriptID](::hermes::JSONEmitter &json) { + json.emitKey("start"); + json.openDict(); + json.emitKeyValue("scriptId", scriptID); + json.emitKeyValue("lineNumber", 1); + json.emitKeyValue("columnNumber", 0); + json.closeDict(); + json.emitKey("end"); + json.openDict(); + json.emitKeyValue("scriptId", scriptID); + json.emitKeyValue("lineNumber", 1); + json.emitKeyValue("columnNumber", 0); + json.closeDict(); + }); + auto resp = ensureGetPossibleBreakpointsResponse(waitForMessage(), msgId++); + EXPECT_TRUE(resp.locations.empty()); + + sendAndCheckResponse("Debugger.resume", msgId++); + ensureNotification(waitForMessage(), "Debugger.resumed"); +} + +TEST_F(CDPAgentTest, DebuggerGetPossibleBreakpointsStatementGranularity) { + int msgId = 1; + + sendAndCheckResponse("Debugger.enable", msgId++); + + // `var z = a + b + c;` is a single statement that compiles to several + // sub-expression instructions (loads and adds). Reading variables (rather + // than literals) prevents constant folding so those instructions survive. + // We expect one break location for the statement, not one per operand. + scheduleScript(R"( + var a = 1; var b = 2; var c = 3; // line 1 (CDP): three statements + debugger; // line 2 (CDP): pause + var z = a + b + c; // line 3 (CDP): one statement + )"); + + auto note = expectNotification("Debugger.scriptParsed"); + auto scriptID = jsonScope_.getString(note, {"params", "scriptId"}); + + ensurePaused(waitForMessage(), "other", {{"global", 2, 1}}); + + sendRequest( + "Debugger.getPossibleBreakpoints", + msgId, + [scriptID](::hermes::JSONEmitter &json) { + json.emitKey("start"); + json.openDict(); + json.emitKeyValue("scriptId", scriptID); + json.emitKeyValue("lineNumber", 0); + json.emitKeyValue("columnNumber", 0); + json.closeDict(); + }); + + auto resp = ensureGetPossibleBreakpointsResponse(waitForMessage(), msgId++); + + // The single-statement line collapses to exactly one statement-boundary + // (typeless) location, rather than one per sub-expression instruction. + // (Other typed locations, such as the function's implicit return, may also + // land on this line; those are not statement boundaries.) + int statementBoundariesOnVarZLine = 0; + for (const m::debugger::BreakLocation &location : resp.locations) { + if (location.lineNumber == 3 && !location.type.has_value()) { + statementBoundariesOnVarZLine++; + } + } + EXPECT_EQ(statementBoundariesOnVarZLine, 1); + + sendAndCheckResponse("Debugger.resume", msgId++); + ensureNotification(waitForMessage(), "Debugger.resumed"); +} + TEST_F(CDPAgentTest, DebuggerSetBreakpointByUrl) { int msgId = 1; diff --git a/unittests/API/CDPJSONHelpers.cpp b/unittests/API/CDPJSONHelpers.cpp index 5111498a420..b3e3d05c489 100644 --- a/unittests/API/CDPJSONHelpers.cpp +++ b/unittests/API/CDPJSONHelpers.cpp @@ -448,6 +448,16 @@ m::debugger::BreakpointId ensureSetBreakpointByUrlResponse( return resp.breakpointId; } +m::debugger::GetPossibleBreakpointsResponse +ensureGetPossibleBreakpointsResponse(const std::string &message, int id) { + JSLexer::Allocator allocator; + JSONFactory factory(allocator); + auto resp = mustMake( + mustParseStrAsJsonObj(message, factory)); + EXPECT_EQ(resp.id, id); + return resp; +} + std::string serializeRuntimeCallFunctionOnRequest( const m::runtime::CallFunctionOnRequest &req) { return req.toJsonStr(); diff --git a/unittests/API/CDPJSONHelpers.h b/unittests/API/CDPJSONHelpers.h index 227b3e89ae0..68ad753d308 100644 --- a/unittests/API/CDPJSONHelpers.h +++ b/unittests/API/CDPJSONHelpers.h @@ -216,6 +216,11 @@ m::debugger::BreakpointId ensureSetBreakpointByUrlResponse( int id, std::vector locations); +/// Ensure that \p message is a Debugger.getPossibleBreakpoints response with +/// the given \p id, and return the parsed response. +m::debugger::GetPossibleBreakpointsResponse +ensureGetPossibleBreakpointsResponse(const std::string &message, int id); + m::runtime::GetPropertiesResponse ensureProps( const std::string &message, const std::unordered_map &infos,