@@ -8,6 +8,8 @@ import 'package:dwds/src/debugging/location.dart';
88import 'package:dwds/src/debugging/modules.dart' ;
99import 'package:dwds/src/loaders/strategy.dart' ;
1010import 'package:dwds/src/services/expression_compiler.dart' ;
11+ import 'package:dwds/src/services/javascript_builder.dart' ;
12+ import 'package:dwds/src/utilities/conversions.dart' ;
1113import 'package:dwds/src/utilities/domain.dart' ;
1214import 'package:dwds/src/utilities/objects.dart' as chrome;
1315import '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