blob: ffc3ae47c4f4281e340d3196a60e3efe15209873 [file]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#include "RuntimeDebugPch.h"
namespace Js
{
DebugDocument::DebugDocument(Utf8SourceInfo* utf8SourceInfo, Js::FunctionBody* functionBody) :
utf8SourceInfo(utf8SourceInfo),
m_breakpointList(nullptr)
{
Assert(utf8SourceInfo != nullptr);
if (functionBody != nullptr)
{
this->functionBody.Root(functionBody, this->utf8SourceInfo->GetScriptContext()->GetRecycler());
}
}
DebugDocument::~DebugDocument()
{
Assert(this->utf8SourceInfo == nullptr);
Assert(this->m_breakpointList == nullptr);
}
void DebugDocument::CloseDocument()
{
if (this->m_breakpointList != nullptr)
{
this->ClearAllBreakPoints();
}
Assert(this->utf8SourceInfo != nullptr);
if (functionBody)
{
functionBody.Unroot(this->utf8SourceInfo->GetScriptContext()->GetRecycler());
}
this->utf8SourceInfo = nullptr;
}
BreakpointProbeList* DebugDocument::GetBreakpointList()
{
if (m_breakpointList != nullptr)
{
return m_breakpointList;
}
ScriptContext * scriptContext = this->utf8SourceInfo->GetScriptContext();
if (scriptContext == nullptr || scriptContext->IsClosed())
{
return nullptr;
}
ArenaAllocator* diagnosticArena = scriptContext->AllocatorForDiagnostics();
AssertMem(diagnosticArena);
m_breakpointList = this->NewBreakpointList(diagnosticArena);
return m_breakpointList;
}
BreakpointProbeList* DebugDocument::NewBreakpointList(ArenaAllocator* arena)
{
return BreakpointProbeList::New(arena);
}
HRESULT DebugDocument::SetBreakPoint(int32 ibos, BREAKPOINT_STATE breakpointState)
{
ScriptContext* scriptContext = this->utf8SourceInfo->GetScriptContext();
if (scriptContext == nullptr || scriptContext->IsClosed())
{
return E_UNEXPECTED;
}
StatementLocation statement;
if (!this->GetStatementLocation(ibos, &statement))
{
return E_FAIL;
}
this->SetBreakPoint(statement, breakpointState);
return S_OK;
}
BreakpointProbe* DebugDocument::SetBreakPoint(StatementLocation statement, BREAKPOINT_STATE bps)
{
ScriptContext* scriptContext = this->utf8SourceInfo->GetScriptContext();
if (scriptContext == nullptr || scriptContext->IsClosed())
{
return nullptr;
}
switch (bps)
{
default:
AssertMsg(FALSE, "Bad breakpoint state");
// Fall thru
case BREAKPOINT_DISABLED:
case BREAKPOINT_DELETED:
{
BreakpointProbeList* pBreakpointList = this->GetBreakpointList();
if (pBreakpointList)
{
ArenaAllocator arena(_u("TemporaryBreakpointList"), scriptContext->GetThreadContext()->GetDebugManager()->GetDiagnosticPageAllocator(), Throw::OutOfMemory);
BreakpointProbeList* pDeleteList = this->NewBreakpointList(&arena);
pBreakpointList->Map([&statement, scriptContext, pDeleteList](int index, BreakpointProbe * breakpointProbe)
{
if (breakpointProbe->Matches(statement.function, statement.statement.begin))
{
scriptContext->GetDebugContext()->GetProbeContainer()->RemoveProbe(breakpointProbe);
pDeleteList->Add(breakpointProbe);
}
});
pDeleteList->Map([pBreakpointList](int index, BreakpointProbe * breakpointProbe)
{
pBreakpointList->Remove(breakpointProbe);
});
pDeleteList->Clear();
}
break;
}
case BREAKPOINT_ENABLED:
{
BreakpointProbe* pProbe = Anew(scriptContext->AllocatorForDiagnostics(), BreakpointProbe, this, statement,
scriptContext->GetThreadContext()->GetDebugManager()->GetNextBreakpointId());
scriptContext->GetDebugContext()->GetProbeContainer()->AddProbe(pProbe);
BreakpointProbeList* pBreakpointList = this->GetBreakpointList();
pBreakpointList->Add(pProbe);
return pProbe;
break;
}
}
return nullptr;
}
void DebugDocument::RemoveBreakpointProbe(BreakpointProbe *probe)
{
Assert(probe);
if (m_breakpointList)
{
m_breakpointList->Remove(probe);
}
}
void DebugDocument::ClearAllBreakPoints(void)
{
if (m_breakpointList != nullptr)
{
m_breakpointList->Clear();
m_breakpointList = nullptr;
}
}
#if ENABLE_TTD
BreakpointProbe* DebugDocument::SetBreakPoint_TTDWbpId(int64 bpId, StatementLocation statement)
{
ScriptContext* scriptContext = this->utf8SourceInfo->GetScriptContext();
BreakpointProbe* pProbe = Anew(scriptContext->AllocatorForDiagnostics(), BreakpointProbe, this, statement, (uint32)bpId);
scriptContext->GetDebugContext()->GetProbeContainer()->AddProbe(pProbe);
BreakpointProbeList* pBreakpointList = this->GetBreakpointList();
pBreakpointList->Add(pProbe);
return pProbe;
}
#endif
Js::BreakpointProbe* DebugDocument::FindBreakpoint(StatementLocation statement)
{
Js::BreakpointProbe* probe = nullptr;
if (m_breakpointList != nullptr)
{
m_breakpointList->MapUntil([&](int index, BreakpointProbe* bpProbe) -> bool
{
if (bpProbe != nullptr && bpProbe->Matches(statement))
{
probe = bpProbe;
return true;
}
return false;
});
}
return probe;
}
bool DebugDocument::FindBPStatementLocation(UINT bpId, StatementLocation * statement)
{
bool foundStatement = false;
if (m_breakpointList != nullptr)
{
m_breakpointList->MapUntil([&](int index, BreakpointProbe* bpProbe) -> bool
{
if (bpProbe != nullptr && bpProbe->GetId() == bpId)
{
bpProbe->GetStatementLocation(statement);
foundStatement = true;
return true;
}
return false;
});
}
return foundStatement;
}
BOOL DebugDocument::GetStatementSpan(int32 ibos, StatementSpan* pStatement)
{
StatementLocation statement;
if (GetStatementLocation(ibos, &statement))
{
pStatement->ich = statement.statement.begin;
pStatement->cch = statement.statement.end - statement.statement.begin;
return TRUE;
}
return FALSE;
}
FunctionBody * DebugDocument::GetFunctionBodyAt(int32 ibos)
{
StatementLocation location = {};
if (GetStatementLocation(ibos, &location))
{
return location.function;
}
return nullptr;
}
BOOL DebugDocument::HasLineBreak(int32 _start, int32 _end)
{
return this->functionBody->HasLineBreak(_start, _end);
}
BOOL DebugDocument::GetStatementLocation(int32 ibos, StatementLocation* plocation)
{
if (ibos < 0)
{
return FALSE;
}
ScriptContext* scriptContext = this->utf8SourceInfo->GetScriptContext();
if (scriptContext == nullptr || scriptContext->IsClosed())
{
return FALSE;
}
uint32 ubos = static_cast<uint32>(ibos);
// Getting the appropriate statement on the asked position works on the heuristic which requires two
// probable candidates. These candidates will be closest to the ibos where first.range.start < ibos and
// second.range.start >= ibos. They will be fetched out by going into each FunctionBody.
StatementLocation candidateMatch1 = {};
StatementLocation candidateMatch2 = {};
this->utf8SourceInfo->MapFunction([&](FunctionBody* pFuncBody)
{
uint32 functionStart = pFuncBody->StartInDocument();
uint32 functionEnd = functionStart + pFuncBody->LengthInBytes();
// For the first candidate, we should allow the current function to participate if its range
// (instead of just start offset) is closer to the ubos compared to already found candidate1.
if (candidateMatch1.function == nullptr ||
((candidateMatch1.statement.begin <= static_cast<int>(functionStart) ||
candidateMatch1.statement.end <= static_cast<int>(functionEnd)) &&
ubos > functionStart) ||
candidateMatch2.function == nullptr ||
(candidateMatch2.statement.begin > static_cast<int>(functionStart) &&
ubos <= functionStart) ||
(functionStart <= ubos &&
ubos < functionEnd))
{
// We need to find out two possible candidate from the current FunctionBody.
pFuncBody->FindClosestStatements(ibos, &candidateMatch1, &candidateMatch2);
}
});
if (candidateMatch1.function == nullptr && candidateMatch2.function == nullptr)
{
return FALSE; // No Match found
}
if (candidateMatch1.function == nullptr || candidateMatch2.function == nullptr)
{
*plocation = (candidateMatch1.function == nullptr) ? candidateMatch2 : candidateMatch1;
return TRUE;
}
// If one of the func is inner to another one, and ibos is in the inner one, disregard the outer one/let the inner one win.
// See WinBlue 575634. Scenario is like this: var foo = function () {this;} -- and BP is set to 'this;' 'function'.
if (candidateMatch1.function != candidateMatch2.function)
{
Assert(candidateMatch1.function && candidateMatch2.function);
regex::Interval func1Range(candidateMatch1.function->StartInDocument());
func1Range.End(func1Range.Begin() + candidateMatch1.function->LengthInBytes());
regex::Interval func2Range(candidateMatch2.function->StartInDocument());
func2Range.End(func2Range.Begin() + candidateMatch2.function->LengthInBytes());
// If cursor (ibos) is just after the closing braces of the inner function then we can't
// directly choose inner function and have to make line break check, so fallback
// function foo(){function bar(){var y=1;}#var x=1;bar();}foo(); - ibos is #
if (func1Range.Includes(func2Range) && func2Range.Includes(ibos) && func2Range.End() != ibos)
{
*plocation = candidateMatch2;
return TRUE;
}
else if (func2Range.Includes(func1Range) && func1Range.Includes(ibos) && func1Range.End() != ibos)
{
*plocation = candidateMatch1;
return TRUE;
}
}
// At this point we have both candidate to consider.
Assert(candidateMatch1.statement.begin < candidateMatch2.statement.begin);
Assert(candidateMatch1.statement.begin < ibos);
Assert(candidateMatch2.statement.begin >= ibos);
// Default selection
*plocation = candidateMatch1;
// If the second candidate start at ibos or
// if the first candidate has line break between ibos and the second candidate is on the same line as ibos
// then consider the second one.
BOOL fNextHasLineBreak = this->HasLineBreak(ibos, candidateMatch2.statement.begin);
if ((candidateMatch2.statement.begin == ibos)
|| (this->HasLineBreak(candidateMatch1.statement.begin, ibos) && !fNextHasLineBreak))
{
*plocation = candidateMatch2;
}
// If ibos is out of the range of first candidate, choose second candidate if ibos is on the same line as second candidate
// or ibos is not on the same line of the end of the first candidate.
else if (candidateMatch1.statement.end < ibos && (!fNextHasLineBreak || this->HasLineBreak(candidateMatch1.statement.end, ibos)))
{
*plocation = candidateMatch2;
}
return TRUE;
}
}