blob: b230d613b2de06927f980c4a4a2bd30dd2d7215e [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
import 'dart:async';
import 'dart:math' as math;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
hide StackTrace;
import '../loaders/strategy.dart';
import '../services/chrome_proxy_service.dart';
import '../utilities/conversions.dart';
import '../utilities/dart_uri.dart';
import '../utilities/domain.dart';
import '../utilities/objects.dart' show Property;
import '../utilities/shared.dart';
import 'dart_scope.dart';
import 'frame_computer.dart';
import 'location.dart';
import 'remote_debugger.dart';
import 'skip_list.dart';
/// Converts from ExceptionPauseMode strings to [PauseState] enums.
///
/// Values defined in:
/// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#method-setPauseOnExceptions
const _pauseModePauseStates = {
'none': PauseState.none,
'all': PauseState.all,
'unhandled': PauseState.uncaught,
};
class Debugger extends Domain {
static final logger = Logger('Debugger');
final RemoteDebugger _remoteDebugger;
final StreamNotify _streamNotify;
final Locations _locations;
final SkipLists _skipLists;
final String _root;
Debugger._(
this._remoteDebugger,
this._streamNotify,
AppInspectorProvider provider,
this._locations,
this._skipLists,
this._root,
) : _breakpoints = _Breakpoints(
locations: _locations,
provider: provider,
remoteDebugger: _remoteDebugger,
root: _root),
super(provider);
/// The breakpoints we have set so far, indexable by either
/// Dart or JS ID.
final _Breakpoints _breakpoints;
PauseState _pauseState = PauseState.none;
String get pauseState => _pauseModePauseStates.entries
.firstWhere((entry) => entry.value == _pauseState)
.key;
/// The JS frames at the current paused location.
///
/// The most important thing here is that frames are identified by
/// frameIndex in the Dart API, but by frame Id in Chrome, so we need
/// to keep the JS frames and their Ids around.
FrameComputer stackComputer;
bool _isStepping = false;
Future<Success> pause() async {
_isStepping = false;
var result = await _remoteDebugger.pause();
handleErrorIfPresent(result);
return Success();
}
Future<Success> setExceptionPauseMode(String isolateId, String mode) async {
checkIsolate('setExceptionPauseMode', isolateId);
mode = mode?.toLowerCase();
if (!_pauseModePauseStates.containsKey(mode)) {
throwInvalidParam('setExceptionPauseMode', 'Unsupported mode: $mode');
}
_pauseState = _pauseModePauseStates[mode];
await _remoteDebugger.setPauseOnExceptions(_pauseState);
return Success();
}
/// Resumes the debugger.
///
/// Step parameter options:
/// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#resume
///
/// If the step parameter is not provided, the program will resume regular
/// execution.
///
/// If the step parameter is provided, it indicates what form of
/// single-stepping to use.
///
/// Note that stepping will automatically continue until Chrome is paused at
/// a location for which we have source information.
Future<Success> resume(String isolateId,
{String step, int frameIndex}) async {
checkIsolate('resume', isolateId);
if (frameIndex != null) {
throw ArgumentError('FrameIndex is currently unsupported.');
}
WipResponse result;
if (step != null) {
_isStepping = true;
switch (step) {
case 'Over':
result = await _remoteDebugger.stepOver();
break;
case 'Out':
result = await _remoteDebugger.stepOut();
break;
case 'Into':
result = await _remoteDebugger.stepInto();
break;
default:
throwInvalidParam('resume', 'Unexpected value for step: $step');
}
} else {
_isStepping = false;
result = await _remoteDebugger.resume();
}
handleErrorIfPresent(result);
return Success();
}
/// Returns the current Dart stack for the paused debugger.
///
/// Returns null if the debugger is not paused.
///
/// The returned stack will contain up to [limit] frames if provided.
Future<Stack> getStack(String isolateId, {int limit}) async {
checkIsolate('getStack', isolateId);
if (stackComputer == null) {
throw RPCError('getStack', RPCError.kInternalError,
'Cannot compute stack when application is not paused');
}
var frames = await stackComputer.calculateFrames(limit: limit);
return Stack(
frames: frames,
messages: [],
truncated: limit != null && frames.length == limit);
}
static Future<Debugger> create(
RemoteDebugger remoteDebugger,
StreamNotify streamNotify,
AppInspectorProvider appInspectorProvider,
Locations locations,
SkipLists skipLists,
String root,
) async {
var debugger = Debugger._(
remoteDebugger,
streamNotify,
appInspectorProvider,
locations,
skipLists,
root,
);
await debugger._initialize();
return debugger;
}
Future<Null> _initialize() async {
// We must add a listener before enabling the debugger otherwise we will
// miss events.
// Allow a null debugger/connection for unit tests.
runZonedGuarded(() {
_remoteDebugger?.onPaused?.listen(_pauseHandler);
_remoteDebugger?.onResumed?.listen(_resumeHandler);
}, (e, StackTrace s) {
logger.warning('Error handling Chrome event', e, s);
});
handleErrorIfPresent(await _remoteDebugger?.enablePage());
handleErrorIfPresent(await _remoteDebugger?.enable() as WipResponse);
// Enable collecting information about async frames when paused.
handleErrorIfPresent(await _remoteDebugger
?.sendCommand('Debugger.setAsyncCallStackDepth', params: {
'maxDepth': 128,
}));
}
/// Resumes the Isolate from start.
///
/// The JS VM is technically not paused at the start of the Isolate so there
/// will not be a corresponding [DebuggerResumedEvent].
Future<void> resumeFromStart() => _resumeHandler(null);
/// Notify the debugger the [Isolate] is paused at the application start.
void notifyPausedAtStart() async {
stackComputer = FrameComputer(this, []);
}
/// Add a breakpoint at the given position.
///
/// Note that line and column are Dart source locations and are one-based.
Future<Breakpoint> addBreakpoint(
String isolateId,
String scriptId,
int line, {
int column,
}) async {
checkIsolate('addBreakpoint', isolateId);
final breakpoint = await _breakpoints.add(scriptId, line);
_notifyBreakpoint(breakpoint);
return breakpoint;
}
Future<ScriptRef> _updatedScriptRefFor(Breakpoint breakpoint) async {
var oldRef = (breakpoint.location as SourceLocation).script;
var dartUri = DartUri(oldRef.uri, _root);
return await inspector.scriptRefFor(dartUri.serverPath);
}
Future<void> reestablishBreakpoints(
Set<Breakpoint> previousBreakpoints,
Set<Breakpoint> disabledBreakpoints,
) async {
// Previous breakpoints were never removed from Chrome since we use
// `setBreakpointByUrl`. We simply need to update the references.
for (var breakpoint in previousBreakpoints) {
var scriptRef = await _updatedScriptRefFor(breakpoint);
var updatedLocation = await _locations.locationForDart(
DartUri(scriptRef.uri, _root), _lineNumberFor(breakpoint));
var updatedBreakpoint = _breakpoints._dartBreakpoint(
scriptRef, updatedLocation, breakpoint.id);
_breakpoints._note(
bp: updatedBreakpoint,
jsId: _breakpoints._jsIdByDartId[updatedBreakpoint.id]);
_notifyBreakpoint(updatedBreakpoint);
}
// Disabled breakpoints were actually removed from Chrome so simply add
// them back.
for (var breakpoint in disabledBreakpoints) {
await addBreakpoint(
inspector.isolate.id,
(await _updatedScriptRefFor(breakpoint)).id,
_lineNumberFor(breakpoint));
}
}
void _notifyBreakpoint(Breakpoint breakpoint) {
final event = Event(
kind: EventKind.kBreakpointAdded,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: inspector.isolateRef,
);
event.breakpoint = breakpoint;
_streamNotify('Debug', event);
}
/// Remove a Dart breakpoint.
Future<Success> removeBreakpoint(
String isolateId, String breakpointId) async {
checkIsolate('removeBreakpoint', isolateId);
if (!_breakpoints._bpByDartId.containsKey(breakpointId)) {
throwInvalidParam(
'removeBreakpoint', 'invalid breakpoint id $breakpointId');
}
final jsId = _breakpoints.jsId(breakpointId);
await _removeBreakpoint(jsId);
var bp = await _breakpoints.remove(jsId: jsId, dartId: breakpointId);
if (bp != null) {
_streamNotify(
'Debug',
Event(
kind: EventKind.kBreakpointRemoved,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: inspector.isolateRef)
..breakpoint = bp,
);
}
return Success();
}
/// Call the Chrome protocol removeBreakpoint.
Future<void> _removeBreakpoint(String breakpointId) async {
try {
var response = await _remoteDebugger.removeBreakpoint(breakpointId);
handleErrorIfPresent(response);
} on WipError catch (e) {
throw RPCError('removeBreakpoint', 102, '$e');
}
}
/// Returns source [Location] for the paused event.
///
/// If we do not have [Location] data for the embedded JS location, null is
/// returned.
Future<Location> _sourceLocation(DebuggerPausedEvent e) {
var frame = e.params['callFrames'][0];
var location = frame['location'];
return _locations.locationForJs(
frame['url'] as String, (location['lineNumber'] as int) + 1);
}
/// The variables visible in a frame in Dart protocol [BoundVariable] form.
Future<List<BoundVariable>> variablesFor(WipCallFrame frame) async {
// TODO(alanknight): Can these be moved to dart_scope.dart?
var properties = await visibleProperties(debugger: this, frame: frame);
var boundVariables = await Future.wait(
properties.map((property) async => await _boundVariable(property)),
);
// Filter out variables that do not come from dart code, such as native
// JavaScript objects
return boundVariables
.where((bv) => bv != null && !isNativeJsObject(bv.value as InstanceRef))
.toList();
}
Future<BoundVariable> _boundVariable(Property property) async {
// We return one level of properties from this object. Sub-properties are
// another round trip.
var instanceRef =
await inspector.instanceHelper.instanceRefFor(property.value);
// Skip null instance refs, which we get for weird objects, e.g.
// properties that are getter/setter pairs.
// TODO(alanknight): Handle these properly.
if (instanceRef == null) return null;
return BoundVariable(
name: property.name,
value: instanceRef,
// TODO(grouma) - Provide actual token positions.
declarationTokenPos: -1,
scopeStartTokenPos: -1,
scopeEndTokenPos: -1,
);
}
/// Find a sub-range of the entries for a Map/List when offset and/or count
/// have been specified on a getObject request.
///
/// If the object referenced by [id] is not a system List or Map then this
/// will just return a RemoteObject for it and ignore [offset], [count] and
/// [length]. If it is, then [length] should be the number of entries in the
/// List/Map and [offset] and [count] should indicate the desired range.
Future<RemoteObject> _subrange(
String id, int offset, int count, int length) async {
// TODO(#809): Sometimes we already know the type of the object, and
// we could take advantage of that to short-circuit.
var receiver = remoteObjectFor(id);
var end = count == null ? null : math.min(offset + count, length);
var actualCount = count ?? length - offset;
var args =
[offset, actualCount, end].map(dartIdFor).map(remoteObjectFor).toList();
// If this is a List, just call sublist. If it's a Map, get the entries, but
// avoid doing a toList on a large map using skip/take to get the section we
// want. To make those alternatives easier in JS, pass both count and end.
var expression = '''
function (offset, count, end) {
const sdk = ${globalLoadStrategy.loadModuleSnippet}("dart_sdk");
if (sdk.core.Map.is(this)) {
const entries = sdk.dart.dload(this, "entries");
const skipped = sdk.dart.dsend(entries, "skip", [offset])
const taken = sdk.dart.dsend(skipped, "take", [count]);
return sdk.dart.dsend(taken, "toList", []);
} else if (sdk.core.List.is(this)) {
return sdk.dart.dsendRepl(this, "sublist", [offset, end]);
} else {
return this;
}
}
''';
return await inspector.jsCallFunctionOn(receiver, expression, args);
}
/// Calls the Chrome Runtime.getProperties API for the object with [objectId].
///
/// Note that the property names are JS names, e.g.
/// Symbol(DartClass.actualName) and will need to be converted. For a system
/// List or Map, [offset] and/or [count] can be provided to indicate a desired
/// range of entries. If those are provided, then [length] should also be
/// provided to indicate the total length of the List/Map.
Future<List<Property>> getProperties(String objectId,
{int offset, int count, int length}) async {
var rangeId = objectId;
if (offset != null || count != null) {
var range = await _subrange(objectId, offset ?? 0, count, length);
rangeId = range.objectId;
}
var response =
await _remoteDebugger.sendCommand('Runtime.getProperties', params: {
'objectId': rangeId,
'ownProperties': true,
});
var jsProperties = response.result['result'];
var properties = (jsProperties as List)
.map<Property>((each) => Property(each as Map<String, dynamic>))
.toList();
return properties;
}
/// Returns a Dart [Frame] for a JS [frame].
Future<Frame> calculateDartFrameFor(
WipCallFrame frame,
int frameIndex, {
bool populateVariables = true,
}) async {
var location = frame.location;
// Chrome is 0 based. Account for this.
var line = location.lineNumber + 1;
var column = location.columnNumber + 1;
// TODO(sdk/issues/37240) - ideally we look for an exact location instead
// of the closest location on a given line.
Location bestLocation;
for (var location in await _locations.locationsForUrl(frame.url)) {
if (location.jsLocation.line == line) {
bestLocation ??= location;
if ((location.jsLocation.column - column).abs() <
(bestLocation.jsLocation.column - column).abs()) {
bestLocation = location;
}
}
}
if (bestLocation == null) return null;
var script =
await inspector?.scriptRefFor(bestLocation.dartLocation.uri.serverPath);
// We think we found a location, but for some reason we can't find the
// script. Just drop the frame.
// TODO(#700): Understand when this can happen and have a better fix.
if (script == null) return null;
var functionName =
_prettifyMember((frame.functionName ?? '').split('.').last);
var codeRefName = functionName.isEmpty ? '<closure>' : functionName;
var dartFrame = Frame(
index: frameIndex,
code: CodeRef(
name: codeRefName,
kind: CodeKind.kDart,
id: createId(),
),
location: SourceLocation(tokenPos: bestLocation.tokenPos, script: script),
kind: FrameKind.kRegular,
);
// Don't populate variables for async frames.
if (populateVariables) {
dartFrame.vars = await variablesFor(frame);
}
return dartFrame;
}
/// Handles pause events coming from the Chrome connection.
Future<void> _pauseHandler(DebuggerPausedEvent e) async {
if (inspector == null) return;
var isolate = inspector.isolate;
if (isolate == null) return;
Event event;
var timestamp = DateTime.now().millisecondsSinceEpoch;
var jsBreakpointIds = e.hitBreakpoints ?? [];
if (jsBreakpointIds.isNotEmpty) {
var breakpointIds = jsBreakpointIds
.map((id) => _breakpoints._dartIdByJsId[id])
// In case the breakpoint was set in Chrome DevTools outside of
// package:dwds.
.where((entry) => entry != null)
.toSet();
var pauseBreakpoints = isolate.breakpoints
.where((bp) => breakpointIds.contains(bp.id))
.toList();
event = Event(
kind: EventKind.kPauseBreakpoint,
timestamp: timestamp,
isolate: inspector.isolateRef)
..pauseBreakpoints = pauseBreakpoints;
} else if (e.reason == 'exception' || e.reason == 'assert') {
InstanceRef exception;
if (e.data is Map<String, dynamic>) {
var map = e.data as Map<String, dynamic>;
if (map['type'] == 'object') {
// The className here is generally 'DartError'.
var obj = RemoteObject(map);
exception = await inspector.instanceHelper.instanceRefFor(obj);
// TODO: The exception object generally doesn't get converted to a
// Dart object (and instead has a classRef name of 'NativeJavaScriptObject').
if (isNativeJsObject(exception)) {
if (obj.description != null) {
// Create a string exception object.
exception = await inspector.instanceHelper
.instanceRefFor(obj.description);
} else {
exception = null;
}
}
}
}
event = Event(
kind: EventKind.kPauseException,
timestamp: timestamp,
isolate: inspector.isolateRef,
exception: exception,
);
} else {
// If we don't have source location continue stepping.
if (_isStepping && (await _sourceLocation(e)) == null) {
var frame = e.params['callFrames'][0];
var url = '${frame["url"]}';
var scriptId = '${frame["location"]["scriptId"]}';
// TODO(grouma) - In the future we should send all previously computed
// skipLists.
await _remoteDebugger.stepInto(params: {
'skipList': await _skipLists.compute(
scriptId,
await _locations.locationsForUrl(url),
)
});
return;
}
event = Event(
kind: EventKind.kPauseInterrupted,
timestamp: timestamp,
isolate: inspector.isolateRef);
}
// Calculate the frames (and handle any exceptions that may occur).
stackComputer = FrameComputer(
this,
e.getCallFrames().toList(),
asyncStackTrace: e.asyncStackTrace,
);
try {
var frames = await stackComputer.calculateFrames(limit: 1);
event.topFrame = frames.isNotEmpty ? frames.first : null;
} catch (e, s) {
// TODO: Return information about the error to the user.
logger.warning('Error calculating Dart frames', e, s);
}
isolate.pauseEvent = event;
_streamNotify('Debug', event);
}
/// Handles resume events coming from the Chrome connection.
Future<void> _resumeHandler(DebuggerResumedEvent _) async {
// We can receive a resume event in the middle of a reload which will result
// in a null isolate.
var isolate = inspector?.isolate;
if (isolate == null) return;
stackComputer = null;
var event = Event(
kind: EventKind.kResume,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: inspector.isolateRef);
isolate.pauseEvent = event;
_streamNotify('Debug', event);
}
/// Evaluate [expression] by calling Chrome's Runtime.evaluate
Future<RemoteObject> evaluate(String expression) async {
try {
return await _remoteDebugger.evaluate(expression);
} on ExceptionDetails catch (e) {
throw ChromeDebugException(
e.json,
evalContents: expression,
additionalDetails: {
'Dart expression': expression,
},
);
}
}
WipCallFrame jsFrameForIndex(int frameIndex) {
if (stackComputer == null) {
throw RPCError('evaluateInFrame', 106,
'Cannot evaluate on a call frame when the program is not paused');
}
return stackComputer.jsFrameForIndex(frameIndex);
}
/// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on
/// the call frame with index [frameIndex] in the currently saved stack.
///
/// If the program is not paused, so there is no current stack, throws a
/// [StateError].
Future<RemoteObject> evaluateJsOnCallFrameIndex(
int frameIndex, String expression) {
return evaluateJsOnCallFrame(
jsFrameForIndex(frameIndex).callFrameId, expression);
}
/// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on
/// the call frame with id [callFrameId].
Future<RemoteObject> evaluateJsOnCallFrame(
String callFrameId, String expression) async {
// TODO(alanknight): Support a version with arguments if needed.
try {
return await _remoteDebugger.evaluateOnCallFrame(callFrameId, expression);
} on ExceptionDetails catch (e) {
throw ChromeDebugException(
e.json,
evalContents: expression,
additionalDetails: {
'Dart expression': expression,
},
);
}
}
}
bool isNativeJsObject(InstanceRef instanceRef) =>
// New type representation of JS objects reifies them to JavaScriptObject.
(instanceRef?.classRef?.name == 'JavaScriptObject' &&
instanceRef?.classRef?.library?.uri == 'dart:_interceptors') ||
// Old type representation still needed to support older SDK versions.
instanceRef?.classRef?.name == 'NativeJavaScriptObject';
/// Returns the Dart line number for the provided breakpoint.
int _lineNumberFor(Breakpoint breakpoint) =>
int.parse(breakpoint.id.split('#').last);
/// Returns the breakpoint ID for the provided Dart script ID and Dart line
/// number.
String breakpointIdFor(String scriptId, int line) => 'bp/$scriptId#$line';
/// Keeps track of the Dart and JS breakpoint Ids that correspond.
class _Breakpoints extends Domain {
final _dartIdByJsId = <String, String>{};
final _jsIdByDartId = <String, String>{};
final _bpByDartId = <String, Future<Breakpoint>>{};
final Locations locations;
final RemoteDebugger remoteDebugger;
/// The root URI from which the application is served.
final String root;
_Breakpoints({
@required this.locations,
@required AppInspectorProvider provider,
@required this.remoteDebugger,
@required this.root,
}) : super(provider);
Future<Breakpoint> _createBreakpoint(
String id, String scriptId, int line) async {
var dartScript = inspector.scriptWithId(scriptId);
var dartUri = DartUri(dartScript.uri, root);
var location = await locations.locationForDart(dartUri, line);
// TODO: Handle cases where a breakpoint can't be set exactly at that line.
if (location == null) {
throw RPCError(
'addBreakpoint',
102,
'The VM is unable to add a breakpoint '
'at the specified line or function');
}
try {
var dartBreakpoint = _dartBreakpoint(dartScript, location, id);
var jsBreakpointId = await _setJsBreakpoint(location);
_note(jsId: jsBreakpointId, bp: dartBreakpoint);
return dartBreakpoint;
} on WipError catch (wipError) {
throw RPCError('addBreakpoint', 102, '$wipError');
}
}
/// Adds a breakpoint at [scriptId] and [line] or returns an existing one if
/// present.
Future<Breakpoint> add(String scriptId, int line) async {
final id = breakpointIdFor(scriptId, line);
return _bpByDartId.putIfAbsent(
id, () => _createBreakpoint(id, scriptId, line));
}
/// Create a Dart breakpoint at [location] in [dartScript] with [id].
Breakpoint _dartBreakpoint(
ScriptRef dartScript, Location location, String id) {
var breakpoint = Breakpoint(
id: id,
breakpointNumber: int.parse(createId()),
resolved: true,
location: SourceLocation(script: dartScript, tokenPos: location.tokenPos),
enabled: true,
)..id = id;
return breakpoint;
}
/// Calls the Chrome protocol setBreakpoint and returns the remote ID.
Future<String> _setJsBreakpoint(Location location) async {
// Location is 0 based according to:
// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#type-Location
// The module can be loaded from a nested path and contain an ETAG suffix.
var urlRegex = '.*${location.jsLocation.module}.*';
var response = await remoteDebugger
.sendCommand('Debugger.setBreakpointByUrl', params: {
'urlRegex': urlRegex,
'lineNumber': location.jsLocation.line - 1,
});
return response.result['breakpointId'] as String;
}
/// Records the internal Dart <=> JS breakpoint id mapping and adds the
/// breakpoint to the current isolates list of breakpoints.
void _note({@required Breakpoint bp, @required String jsId}) {
_dartIdByJsId[jsId] = bp.id;
_jsIdByDartId[bp.id] = jsId;
var isolate = inspector.isolate;
isolate?.breakpoints?.add(bp);
}
Future<Breakpoint> remove({
@required String jsId,
@required String dartId,
}) async {
var isolate = inspector.isolate;
_dartIdByJsId.remove(jsId);
_jsIdByDartId.remove(dartId);
isolate?.breakpoints?.removeWhere((b) => b.id == dartId);
return await _bpByDartId.remove(dartId);
}
String jsId(String dartId) => _jsIdByDartId[dartId];
}
final escapedPipe = '\$124';
final escapedPound = '\$35';
/// Reformats a JS member name to make it look more Dart-like.
///
/// Logic copied from build/build_web_compilers/web/stack_trace_mapper.dart.
/// TODO(https://github.com/dart-lang/sdk/issues/38869): Remove this logic when
/// DDC stack trace deobfuscation is overhauled.
String _prettifyMember(String member) {
member = member.replaceAll(escapedPipe, '|');
if (member.contains('|')) {
return _prettifyExtension(member);
} else {
if (member.startsWith('[') && member.endsWith(']')) {
member = member.substring(1, member.length - 1);
}
return member;
}
}
/// Reformats a JS member name as an extension method invocation.
String _prettifyExtension(String member) {
var isSetter = false;
var pipeIndex = member.indexOf('|');
var spaceIndex = member.indexOf(' ');
var poundIndex = member.indexOf(escapedPound);
if (spaceIndex >= 0) {
// Here member is a static field or static getter/setter.
isSetter = member.substring(0, spaceIndex) == 'set';
member = member.substring(spaceIndex + 1, member.length);
} else if (poundIndex >= 0) {
// Here member is a tearoff or local property getter/setter.
isSetter = member.substring(pipeIndex + 1, poundIndex) == 'set';
member = member.replaceRange(pipeIndex + 1, poundIndex + 3, '');
} else {
var body = member.substring(pipeIndex + 1, member.length);
if (body.startsWith('unary') || body.startsWith('\$')) {
// Here member's an operator, so it's safe to unescape everything lazily.
member = _unescape(member);
}
}
member = member.replaceAll('|', '.');
return isSetter ? '$member=' : member;
}
/// Unescapes a DDC-escaped JS identifier name.
///
/// Identifier names that contain illegal JS characters are escaped by DDC to a
/// decimal representation of the symbol's UTF-16 value.
/// Warning: this greedily escapes characters, so it can be unsafe in the event
/// that an escaped sequence precedes a number literal in the JS name.
String _unescape(String name) {
return name.replaceAllMapped(
RegExp(r'\$[0-9]+'),
(m) =>
String.fromCharCode(int.parse(name.substring(m.start + 1, m.end))));
}