Skip to content

Commit b10d62b

Browse files
author
Anna Gringauze
authored
Support using scope in evaluateInFrame (#2122)
* Support evaluation on frame with scope * Update changelog * Remove old test * Cleanup, add tests and batch JS eval * Update changelog * Remove obsolete test * Try fixing flaky test on windows * Addressed CR comments and simplified scope detection * Added example in comments * removed extra line * Revert change done by mistake * Cleaned up comments * Address CR comments * Fix a bug when adding this to scope
1 parent c0300ce commit b10d62b

File tree

9 files changed

+892
-72
lines changed

9 files changed

+892
-72
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Refactor code for presenting record instances. - [#2074](https://github.com/dart-lang/webdev/pull/2074)
55
- Display record types concisely. - [#2070](https://github.com/dart-lang/webdev/pull/2070)
66
- Display type objects concisely. - [#2103](https://github.com/dart-lang/webdev/pull/2103)
7+
- Support using scope in `ChromeProxyService.evaluateInFrame`. - [#2122](https://github.com/dart-lang/webdev/pull/2122)
78
- Check for new events more often in batched stream. - [#2123](https://github.com/dart-lang/webdev/pull/2123)
89

910
## 19.0.0

dwds/lib/src/services/expression_evaluator.dart

Lines changed: 190 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:dwds/src/debugging/location.dart';
88
import 'package:dwds/src/debugging/modules.dart';
99
import 'package:dwds/src/loaders/strategy.dart';
1010
import 'package:dwds/src/services/expression_compiler.dart';
11+
import 'package:dwds/src/services/javascript_builder.dart';
12+
import 'package:dwds/src/utilities/conversions.dart';
1113
import 'package:dwds/src/utilities/domain.dart';
1214
import 'package:dwds/src/utilities/objects.dart' as chrome;
1315
import 'package:logging/logging.dart';
@@ -144,15 +146,13 @@ class ExpressionEvaluator {
144146
}
145147

146148
// Strip try/catch incorrectly added by the expression compiler.
147-
var jsCode = _maybeStripTryCatch(jsResult);
149+
final jsCode = _maybeStripTryCatch(jsResult);
148150

149151
// Send JS expression to chrome to evaluate.
150-
jsCode = _createJsLambdaWithTryCatch(jsCode, scope.keys);
151-
var result = await _inspector.callFunction(jsCode, scope.values);
152+
var result = await _callJsFunction(jsCode, scope);
152153
result = await _formatEvaluationError(result);
153154

154-
_logger
155-
.finest('Evaluated "$expression" to "$result" for isolate $isolateId');
155+
_logger.finest('Evaluated "$expression" to "${result.json}"');
156156
return result;
157157
}
158158

@@ -168,20 +168,80 @@ class ExpressionEvaluator {
168168
/// [isolateId] current isolate ID.
169169
/// [frameIndex] JavaScript frame to evaluate the expression in.
170170
/// [expression] dart expression to evaluate.
171+
/// [scope] additional scope to use in the expression as a map from
172+
/// variable names to remote object IDs.
173+
///
174+
/////////////////////////////////
175+
/// **Example - without scope**
176+
///
177+
/// To evaluate a dart expression `e`, we perform the following:
178+
///
179+
/// 1. compile dart expression `e` to JavaScript expression `jsExpr`
180+
/// using the expression compiler (i.e. frontend server or expression
181+
/// compiler worker).
182+
///
183+
/// 2. create JavaScript wrapper expression, `jsWrapperExpr`, defined as
184+
///
185+
/// ```JavaScript
186+
/// try {
187+
/// jsExpr;
188+
/// } catch (error) {
189+
/// error.name + ": " + error.message;
190+
/// }
191+
/// ```
192+
///
193+
/// 3. evaluate `JsExpr` using `Debugger.evaluateOnCallFrame` chrome API.
194+
///
195+
/// //////////////////////////
196+
/// **Example - with scope**
197+
///
198+
/// To evaluate a dart expression
199+
/// ```dart
200+
/// this.t + a + x + y
201+
/// ```
202+
/// in a dart scope that defines `a` and `this`, and additional scope
203+
/// `x, y`, we perform the following:
204+
///
205+
/// 1. compile dart function
206+
///
207+
/// ```dart
208+
/// (x, y, a) { return this.t + a + x + y; }
209+
/// ```
210+
///
211+
/// to JavaScript function
212+
///
213+
/// ```jsFunc```
214+
///
215+
/// using the expression compiler (i.e. frontend server or expression
216+
/// compiler worker).
217+
///
218+
/// 2. create JavaScript wrapper function, `jsWrapperFunc`, defined as
219+
///
220+
/// ```JavaScript
221+
/// function (x, y, a, __t$this) {
222+
/// try {
223+
/// return function (x, y, a) {
224+
/// return jsFunc(x, y, a);
225+
/// }.bind(__t$this)(x, y, a);
226+
/// } catch (error) {
227+
/// return error.name + ": " + error.message;
228+
/// }
229+
/// }
230+
/// ```
231+
///
232+
/// 3. collect scope variable object IDs for total scope
233+
/// (original frame scope from WipCallFrame + additional scope passed
234+
/// by the user).
235+
///
236+
/// 4. call `jsWrapperFunc` using `Runtime.callFunctionOn` chrome API
237+
/// with scope variable object IDs passed as arguments.
171238
Future<RemoteObject> evaluateExpressionInFrame(
172239
String isolateId,
173240
int frameIndex,
174241
String expression,
175242
Map<String, String>? scope,
176243
) async {
177-
if (scope != null && scope.isNotEmpty) {
178-
// TODO(annagrin): Implement scope support.
179-
// Issue: https://github.com/dart-lang/webdev/issues/1344
180-
return createError(
181-
EvaluationErrorKind.internal,
182-
'Using scope for expression evaluation in frame '
183-
'is not supported.');
184-
}
244+
scope ??= {};
185245

186246
if (expression.isEmpty) {
187247
return createError(EvaluationErrorKind.invalidInput, expression);
@@ -200,7 +260,7 @@ class ExpressionEvaluator {
200260
final jsLine = jsFrame.location.lineNumber;
201261
final jsScriptId = jsFrame.location.scriptId;
202262
final jsColumn = jsFrame.location.columnNumber;
203-
final jsScope = await _collectLocalJsScope(jsFrame);
263+
final frameScope = await _collectLocalFrameScope(jsFrame);
204264

205265
// Find corresponding dart location and scope.
206266
final url = _debugger.urlForScriptId(jsScriptId);
@@ -240,7 +300,15 @@ class ExpressionEvaluator {
240300
}
241301

242302
_logger.finest('Evaluating "$expression" at $module, '
243-
'$libraryUri:${dartLocation.line}:${dartLocation.column}');
303+
'$libraryUri:${dartLocation.line}:${dartLocation.column} '
304+
'with scope: $scope');
305+
306+
if (scope.isNotEmpty) {
307+
final totalScope = Map<String, String>.from(scope)..addAll(frameScope);
308+
expression = _createDartLambda(expression, totalScope.keys);
309+
}
310+
311+
_logger.finest('Compiling "$expression"');
244312

245313
// Compile expression using an expression compiler, such as
246314
// frontend server or expression compiler worker.
@@ -254,7 +322,7 @@ class ExpressionEvaluator {
254322
dartLocation.line,
255323
dartLocation.column,
256324
{},
257-
jsScope,
325+
frameScope.map((key, value) => MapEntry(key, key)),
258326
module,
259327
expression,
260328
);
@@ -266,19 +334,85 @@ class ExpressionEvaluator {
266334
}
267335

268336
// Strip try/catch incorrectly added by the expression compiler.
269-
var jsCode = _maybeStripTryCatch(jsResult);
337+
final jsCode = _maybeStripTryCatch(jsResult);
270338

271339
// Send JS expression to chrome to evaluate.
272-
jsCode = _createTryCatch(jsCode);
340+
var result = scope.isEmpty
341+
? await _evaluateJsExpressionInFrame(frameIndex, jsCode)
342+
: await _callJsFunctionInFrame(frameIndex, jsCode, scope, frameScope);
273343

274-
// Send JS expression to chrome to evaluate.
275-
var result = await _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode);
276344
result = await _formatEvaluationError(result);
277-
278345
_logger.finest('Evaluated "$expression" to "${result.json}"');
279346
return result;
280347
}
281348

349+
/// Call JavaScript [function] with [scope] on frame [frameIndex].
350+
///
351+
/// Wrap the [function] in a lambda that takes scope variables as parameters.
352+
/// Send JS expression to chrome to evaluate in frame with [frameIndex]
353+
/// with the provided [scope].
354+
///
355+
/// [frameIndex] is the index of the frame to call the function in.
356+
/// [function] is the JS function to evaluate.
357+
/// [scope] is the additional scope as a map from scope variables to
358+
/// remote object IDs.
359+
/// [frameScope] is the original scope as a map from scope variables
360+
/// to remote object IDs.
361+
Future<RemoteObject> _callJsFunctionInFrame(
362+
int frameIndex,
363+
String function,
364+
Map<String, String> scope,
365+
Map<String, String> frameScope,
366+
) async {
367+
final totalScope = Map<String, String>.from(scope)..addAll(frameScope);
368+
final thisObject =
369+
await _debugger.evaluateJsOnCallFrameIndex(frameIndex, 'this');
370+
371+
final thisObjectId = thisObject.objectId;
372+
if (thisObjectId != null) {
373+
totalScope['this'] = thisObjectId;
374+
}
375+
376+
return _callJsFunction(function, totalScope);
377+
}
378+
379+
/// Call the [function] with [scope] as arguments.
380+
///
381+
/// Wrap the [function] in a lambda that takes scope variables as parameters.
382+
/// Send JS expression to chrome to evaluate with the provided [scope].
383+
///
384+
/// [function] is the JS function to evaluate.
385+
/// [scope] is a map from scope variables to remote object IDs.
386+
Future<RemoteObject> _callJsFunction(
387+
String function,
388+
Map<String, String> scope,
389+
) async {
390+
final jsCode = _createEvalFunction(function, scope.keys);
391+
392+
_logger.finest('Evaluating JS: "$jsCode" with scope: $scope');
393+
return _inspector.callFunction(jsCode, scope.values);
394+
}
395+
396+
/// Evaluate JavaScript [expression] on frame [frameIndex].
397+
///
398+
/// Wrap the [expression] in a try/catch expression to catch errors.
399+
/// Send JS expression to chrome to evaluate on frame [frameIndex].
400+
///
401+
/// [frameIndex] is the index of the frame to call the function in.
402+
/// [expression] is the JS function to evaluate.
403+
Future<RemoteObject> _evaluateJsExpressionInFrame(
404+
int frameIndex,
405+
String expression,
406+
) async {
407+
final jsCode = _createEvalExpression(expression);
408+
409+
_logger.finest('Evaluating JS: "$jsCode"');
410+
return _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode);
411+
}
412+
413+
static String? _getObjectId(RemoteObject? object) =>
414+
object?.objectId ?? dartIdFor(object?.value);
415+
282416
RemoteObject _formatCompilationError(String error) {
283417
// Frontend currently gives a text message including library name
284418
// and function name on compilation error. Strip this information
@@ -328,19 +462,25 @@ class ExpressionEvaluator {
328462
return result;
329463
}
330464

331-
Future<Map<String, String>> _collectLocalJsScope(WipCallFrame frame) async {
332-
final jsScope = <String, String>{};
465+
/// Return local scope as a map from variable names to remote object IDs.
466+
///
467+
/// [frame] is the current frame index.
468+
Future<Map<String, String>> _collectLocalFrameScope(
469+
WipCallFrame frame,
470+
) async {
471+
final scope = <String, String>{};
333472

334-
void collectVariables(
335-
Iterable<chrome.Property> variables,
336-
) {
473+
void collectVariables(Iterable<chrome.Property> variables) {
337474
for (var p in variables) {
338475
final name = p.name;
339476
final value = p.value;
340477
// TODO: null values represent variables optimized by v8.
341478
// Show that to the user.
342479
if (name != null && value != null && !_isUndefined(value)) {
343-
jsScope[name] = name;
480+
final objectId = _getObjectId(p.value);
481+
if (objectId != null) {
482+
scope[name] = objectId;
483+
}
344484
}
345485
}
346486
}
@@ -355,16 +495,22 @@ class ExpressionEvaluator {
355495
}
356496
}
357497

358-
return jsScope;
498+
return scope;
359499
}
360500

361501
bool _isUndefined(RemoteObject value) => value.type == 'undefined';
362502

503+
static String _createDartLambda(
504+
String expression,
505+
Iterable<String> params,
506+
) =>
507+
'(${params.join(', ')}) { return $expression; }';
508+
363509
/// Strip try/catch incorrectly added by the expression compiler.
364510
/// TODO: remove adding try/catch block in expression compiler.
365511
/// https://github.com/dart-lang/webdev/issues/1341, then remove
366512
/// this stripping code.
367-
String _maybeStripTryCatch(String jsCode) {
513+
static String _maybeStripTryCatch(String jsCode) {
368514
// Match the wrapping generated by the expression compiler exactly
369515
// so the matching does not succeed naturally after the wrapping is
370516
// removed:
@@ -396,28 +542,22 @@ class ExpressionEvaluator {
396542
return jsCode;
397543
}
398544

399-
String _createJsLambdaWithTryCatch(
400-
String expression,
401-
Iterable<String> params,
402-
) {
403-
final args = params.join(', ');
404-
return ' '
405-
' function($args) {\n'
406-
' try {\n'
407-
' return $expression($args);\n'
408-
' } catch (error) {\n'
409-
' return error.name + ": " + error.message;\n'
410-
' }\n'
411-
'} ';
545+
/// Create JS expression to pass to `Debugger.evaluateOnCallFrame`.
546+
static String _createEvalExpression(String expression) {
547+
final body = expression.split('\n').where((e) => e.isNotEmpty);
548+
549+
return JsBuilder.createEvalExpression(body);
412550
}
413551

414-
String _createTryCatch(String expression) => ' '
415-
' try {\n'
416-
' $expression;\n'
417-
' } catch (error) {\n'
418-
' error.name + ": " + error.message;\n'
419-
' }\n';
552+
/// Create JS function to invoke in `Runtime.callFunctionOn`.
553+
static String _createEvalFunction(
554+
String function,
555+
Iterable<String> params,
556+
) {
557+
final body = function.split('\n').where((e) => e.isNotEmpty);
420558

421-
String _createDartLambda(String expression, Iterable<String> params) =>
422-
'(${params.join(', ')}) => $expression';
559+
return params.contains('this')
560+
? JsBuilder.createEvalBoundFunction(body, params)
561+
: JsBuilder.createEvalStaticFunction(body, params);
562+
}
423563
}

0 commit comments

Comments
 (0)