using Esprima;
using Esprima.Ast;
using Jint.Native;
using Jint.Runtime.Interpreter;
namespace Jint.Runtime.Debugger
{
public enum PauseType
{
Step,
Break,
DebuggerStatement
}
public class DebugHandler
{
public delegate StepMode DebugEventHandler(object sender, DebugInformation e);
private readonly Engine _engine;
private bool _paused;
private int _steppingDepth;
///
/// The Step event is triggered before the engine executes a step-eligible AST node.
///
///
/// If the current step mode is , this event is never triggered. The script may
/// still be paused by a debugger statement or breakpoint, but these will trigger the
/// event.
///
public event DebugEventHandler? Step;
///
/// The Break event is triggered when a breakpoint or debugger statement is hit.
///
///
/// This is event is not triggered if the current script location was reached by stepping. In that case, only
/// the event is triggered.
///
public event DebugEventHandler? Break;
internal DebugHandler(Engine engine, StepMode initialStepMode)
{
_engine = engine;
HandleNewStepMode(initialStepMode);
}
///
/// The location of the current (step-eligible) AST node being executed.
///
///
/// The location is available as long as DebugMode is enabled - i.e. even when not stepping
/// or hitting a breakpoint.
///
public Location? CurrentLocation { get; private set; }
///
/// Collection of active breakpoints for the engine.
///
public BreakPointCollection BreakPoints { get; } = new BreakPointCollection();
///
/// Evaluates a script (expression) within the current execution context.
///
///
/// Internally, this is used for evaluating breakpoint conditions, but may also be used for e.g. watch lists
/// in a debugger.
///
public JsValue Evaluate(Script script)
{
var context = _engine._activeEvaluationContext;
if (context == null)
{
throw new DebugEvaluationException("Jint has no active evaluation context");
}
int callStackSize = _engine.CallStack.Count;
var list = new JintStatementList(null, script.Body);
Completion result;
try
{
result = list.Execute(context);
}
catch (Exception ex)
{
// An error in the evaluation may return a Throw Completion, or it may throw an exception:
throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
}
finally
{
// Restore call stack
while (_engine.CallStack.Count > callStackSize)
{
_engine.CallStack.Pop();
}
}
if (result.Type == CompletionType.Throw)
{
// TODO: Should we return an error here? (avoid exception overhead, since e.g. breakpoint
// evaluation may be high volume.
var error = result.GetValueOrDefault();
var ex = new JavaScriptException(error).SetJavaScriptCallstack(_engine, result.Location);
throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
}
return result.GetValueOrDefault();
}
///
public JsValue Evaluate(string source, ParserOptions? options = null)
{
options ??= new ParserOptions("evaluation");
var parser = new JavaScriptParser(source, options);
try
{
var script = parser.ParseScript();
return Evaluate(script);
}
catch (ParserException ex)
{
throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex);
}
}
internal void OnStep(Node node)
{
// Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
if (_paused)
{
return;
}
_paused = true;
CheckBreakPointAndPause(
new BreakLocation(node.Location.Source!, node.Location.Start),
node: node,
location: null,
returnValue: null);
}
internal void OnReturnPoint(Node functionBody, JsValue returnValue)
{
// Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
if (_paused)
{
return;
}
_paused = true;
var bodyLocation = functionBody.Location;
var functionBodyEnd = bodyLocation.End;
var location = Location.From(functionBodyEnd, functionBodyEnd, bodyLocation.Source);
CheckBreakPointAndPause(
new BreakLocation(bodyLocation.Source!, bodyLocation.End),
node: null,
location: location,
returnValue: returnValue);
}
internal void OnDebuggerStatement(Statement statement)
{
// Don't reenter if we're already paused
if (_paused)
{
return;
}
_paused = true;
bool isStepping = _engine.CallStack.Count <= _steppingDepth;
// Even though we're at a debugger statement, if we're stepping, ignore the statement. OnStep already
// takes care of pausing.
if (!isStepping)
{
Pause(PauseType.DebuggerStatement, statement);
}
_paused = false;
}
private void CheckBreakPointAndPause(
BreakLocation breakLocation,
Node? node,
Location? location = null,
JsValue? returnValue = null)
{
CurrentLocation = location ?? node?.Location;
var breakpoint = BreakPoints.FindMatch(this, breakLocation);
bool isStepping = _engine.CallStack.Count <= _steppingDepth;
if (breakpoint != null || isStepping)
{
// Even if we matched a breakpoint, if we're stepping, the reason we're pausing is the step.
// Still, we need to include the breakpoint at this location, in case the debugger UI needs to update
// e.g. a hit count.
Pause(isStepping ? PauseType.Step : PauseType.Break, node!, location, returnValue, breakpoint);
}
_paused = false;
}
private void Pause(
PauseType type,
Node node,
Location? location = null,
JsValue? returnValue = null,
BreakPoint? breakPoint = null)
{
var info = new DebugInformation(
engine: _engine,
currentNode: node,
currentLocation: location ?? node.Location,
returnValue: returnValue,
currentMemoryUsage: _engine.CurrentMemoryUsage,
pauseType: type,
breakPoint: breakPoint
);
StepMode? result = type switch
{
// Conventionally, sender should be DebugHandler - but Engine is more useful
PauseType.Step => Step?.Invoke(_engine, info),
PauseType.Break => Break?.Invoke(_engine, info),
PauseType.DebuggerStatement => Break?.Invoke(_engine, info),
_ => throw new ArgumentException("Invalid pause type", nameof(type))
};
HandleNewStepMode(result);
}
private void HandleNewStepMode(StepMode? newStepMode)
{
if (newStepMode != null)
{
_steppingDepth = newStepMode switch
{
StepMode.Over => _engine.CallStack.Count,// Resume stepping when back at this level of the stack
StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the stack
StepMode.None => int.MinValue,// Never step
_ => int.MaxValue,// Always step
};
}
}
}
}