diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index f7e395956..9edc39cb5 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -4,6 +4,7 @@ - Refactor code for presenting record instances. - [#2074](https://github.com/dart-lang/webdev/pull/2074) - Display record types concisely. - [#2070](https://github.com/dart-lang/webdev/pull/2070) - Display type objects concisely. - [#2103](https://github.com/dart-lang/webdev/pull/2103) +- Support using scope in `ChromeProxyService.evaluateInFrame`. - [#2122](https://github.com/dart-lang/webdev/pull/2122) - Check for new events more often in batched stream. - [#2123](https://github.com/dart-lang/webdev/pull/2123) ## 19.0.0 diff --git a/dwds/lib/src/services/expression_evaluator.dart b/dwds/lib/src/services/expression_evaluator.dart index 19b45ad3e..614f69e85 100644 --- a/dwds/lib/src/services/expression_evaluator.dart +++ b/dwds/lib/src/services/expression_evaluator.dart @@ -8,6 +8,8 @@ import 'package:dwds/src/debugging/location.dart'; import 'package:dwds/src/debugging/modules.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/services/expression_compiler.dart'; +import 'package:dwds/src/services/javascript_builder.dart'; +import 'package:dwds/src/utilities/conversions.dart'; import 'package:dwds/src/utilities/domain.dart'; import 'package:dwds/src/utilities/objects.dart' as chrome; import 'package:logging/logging.dart'; @@ -144,15 +146,13 @@ class ExpressionEvaluator { } // Strip try/catch incorrectly added by the expression compiler. - var jsCode = _maybeStripTryCatch(jsResult); + final jsCode = _maybeStripTryCatch(jsResult); // Send JS expression to chrome to evaluate. - jsCode = _createJsLambdaWithTryCatch(jsCode, scope.keys); - var result = await _inspector.callFunction(jsCode, scope.values); + var result = await _callJsFunction(jsCode, scope); result = await _formatEvaluationError(result); - _logger - .finest('Evaluated "$expression" to "$result" for isolate $isolateId'); + _logger.finest('Evaluated "$expression" to "${result.json}"'); return result; } @@ -168,20 +168,80 @@ class ExpressionEvaluator { /// [isolateId] current isolate ID. /// [frameIndex] JavaScript frame to evaluate the expression in. /// [expression] dart expression to evaluate. + /// [scope] additional scope to use in the expression as a map from + /// variable names to remote object IDs. + /// + ///////////////////////////////// + /// **Example - without scope** + /// + /// To evaluate a dart expression `e`, we perform the following: + /// + /// 1. compile dart expression `e` to JavaScript expression `jsExpr` + /// using the expression compiler (i.e. frontend server or expression + /// compiler worker). + /// + /// 2. create JavaScript wrapper expression, `jsWrapperExpr`, defined as + /// + /// ```JavaScript + /// try { + /// jsExpr; + /// } catch (error) { + /// error.name + ": " + error.message; + /// } + /// ``` + /// + /// 3. evaluate `JsExpr` using `Debugger.evaluateOnCallFrame` chrome API. + /// + /// ////////////////////////// + /// **Example - with scope** + /// + /// To evaluate a dart expression + /// ```dart + /// this.t + a + x + y + /// ``` + /// in a dart scope that defines `a` and `this`, and additional scope + /// `x, y`, we perform the following: + /// + /// 1. compile dart function + /// + /// ```dart + /// (x, y, a) { return this.t + a + x + y; } + /// ``` + /// + /// to JavaScript function + /// + /// ```jsFunc``` + /// + /// using the expression compiler (i.e. frontend server or expression + /// compiler worker). + /// + /// 2. create JavaScript wrapper function, `jsWrapperFunc`, defined as + /// + /// ```JavaScript + /// function (x, y, a, __t$this) { + /// try { + /// return function (x, y, a) { + /// return jsFunc(x, y, a); + /// }.bind(__t$this)(x, y, a); + /// } catch (error) { + /// return error.name + ": " + error.message; + /// } + /// } + /// ``` + /// + /// 3. collect scope variable object IDs for total scope + /// (original frame scope from WipCallFrame + additional scope passed + /// by the user). + /// + /// 4. call `jsWrapperFunc` using `Runtime.callFunctionOn` chrome API + /// with scope variable object IDs passed as arguments. Future evaluateExpressionInFrame( String isolateId, int frameIndex, String expression, Map? scope, ) async { - if (scope != null && scope.isNotEmpty) { - // TODO(annagrin): Implement scope support. - // Issue: https://github.com/dart-lang/webdev/issues/1344 - return createError( - EvaluationErrorKind.internal, - 'Using scope for expression evaluation in frame ' - 'is not supported.'); - } + scope ??= {}; if (expression.isEmpty) { return createError(EvaluationErrorKind.invalidInput, expression); @@ -200,7 +260,7 @@ class ExpressionEvaluator { final jsLine = jsFrame.location.lineNumber; final jsScriptId = jsFrame.location.scriptId; final jsColumn = jsFrame.location.columnNumber; - final jsScope = await _collectLocalJsScope(jsFrame); + final frameScope = await _collectLocalFrameScope(jsFrame); // Find corresponding dart location and scope. final url = _debugger.urlForScriptId(jsScriptId); @@ -240,7 +300,15 @@ class ExpressionEvaluator { } _logger.finest('Evaluating "$expression" at $module, ' - '$libraryUri:${dartLocation.line}:${dartLocation.column}'); + '$libraryUri:${dartLocation.line}:${dartLocation.column} ' + 'with scope: $scope'); + + if (scope.isNotEmpty) { + final totalScope = Map.from(scope)..addAll(frameScope); + expression = _createDartLambda(expression, totalScope.keys); + } + + _logger.finest('Compiling "$expression"'); // Compile expression using an expression compiler, such as // frontend server or expression compiler worker. @@ -254,7 +322,7 @@ class ExpressionEvaluator { dartLocation.line, dartLocation.column, {}, - jsScope, + frameScope.map((key, value) => MapEntry(key, key)), module, expression, ); @@ -266,19 +334,85 @@ class ExpressionEvaluator { } // Strip try/catch incorrectly added by the expression compiler. - var jsCode = _maybeStripTryCatch(jsResult); + final jsCode = _maybeStripTryCatch(jsResult); // Send JS expression to chrome to evaluate. - jsCode = _createTryCatch(jsCode); + var result = scope.isEmpty + ? await _evaluateJsExpressionInFrame(frameIndex, jsCode) + : await _callJsFunctionInFrame(frameIndex, jsCode, scope, frameScope); - // Send JS expression to chrome to evaluate. - var result = await _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode); result = await _formatEvaluationError(result); - _logger.finest('Evaluated "$expression" to "${result.json}"'); return result; } + /// Call JavaScript [function] with [scope] on frame [frameIndex]. + /// + /// Wrap the [function] in a lambda that takes scope variables as parameters. + /// Send JS expression to chrome to evaluate in frame with [frameIndex] + /// with the provided [scope]. + /// + /// [frameIndex] is the index of the frame to call the function in. + /// [function] is the JS function to evaluate. + /// [scope] is the additional scope as a map from scope variables to + /// remote object IDs. + /// [frameScope] is the original scope as a map from scope variables + /// to remote object IDs. + Future _callJsFunctionInFrame( + int frameIndex, + String function, + Map scope, + Map frameScope, + ) async { + final totalScope = Map.from(scope)..addAll(frameScope); + final thisObject = + await _debugger.evaluateJsOnCallFrameIndex(frameIndex, 'this'); + + final thisObjectId = thisObject.objectId; + if (thisObjectId != null) { + totalScope['this'] = thisObjectId; + } + + return _callJsFunction(function, totalScope); + } + + /// Call the [function] with [scope] as arguments. + /// + /// Wrap the [function] in a lambda that takes scope variables as parameters. + /// Send JS expression to chrome to evaluate with the provided [scope]. + /// + /// [function] is the JS function to evaluate. + /// [scope] is a map from scope variables to remote object IDs. + Future _callJsFunction( + String function, + Map scope, + ) async { + final jsCode = _createEvalFunction(function, scope.keys); + + _logger.finest('Evaluating JS: "$jsCode" with scope: $scope'); + return _inspector.callFunction(jsCode, scope.values); + } + + /// Evaluate JavaScript [expression] on frame [frameIndex]. + /// + /// Wrap the [expression] in a try/catch expression to catch errors. + /// Send JS expression to chrome to evaluate on frame [frameIndex]. + /// + /// [frameIndex] is the index of the frame to call the function in. + /// [expression] is the JS function to evaluate. + Future _evaluateJsExpressionInFrame( + int frameIndex, + String expression, + ) async { + final jsCode = _createEvalExpression(expression); + + _logger.finest('Evaluating JS: "$jsCode"'); + return _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode); + } + + static String? _getObjectId(RemoteObject? object) => + object?.objectId ?? dartIdFor(object?.value); + RemoteObject _formatCompilationError(String error) { // Frontend currently gives a text message including library name // and function name on compilation error. Strip this information @@ -328,19 +462,25 @@ class ExpressionEvaluator { return result; } - Future> _collectLocalJsScope(WipCallFrame frame) async { - final jsScope = {}; + /// Return local scope as a map from variable names to remote object IDs. + /// + /// [frame] is the current frame index. + Future> _collectLocalFrameScope( + WipCallFrame frame, + ) async { + final scope = {}; - void collectVariables( - Iterable variables, - ) { + void collectVariables(Iterable variables) { for (var p in variables) { final name = p.name; final value = p.value; // TODO: null values represent variables optimized by v8. // Show that to the user. if (name != null && value != null && !_isUndefined(value)) { - jsScope[name] = name; + final objectId = _getObjectId(p.value); + if (objectId != null) { + scope[name] = objectId; + } } } } @@ -355,16 +495,22 @@ class ExpressionEvaluator { } } - return jsScope; + return scope; } bool _isUndefined(RemoteObject value) => value.type == 'undefined'; + static String _createDartLambda( + String expression, + Iterable params, + ) => + '(${params.join(', ')}) { return $expression; }'; + /// Strip try/catch incorrectly added by the expression compiler. /// TODO: remove adding try/catch block in expression compiler. /// https://github.com/dart-lang/webdev/issues/1341, then remove /// this stripping code. - String _maybeStripTryCatch(String jsCode) { + static String _maybeStripTryCatch(String jsCode) { // Match the wrapping generated by the expression compiler exactly // so the matching does not succeed naturally after the wrapping is // removed: @@ -396,28 +542,22 @@ class ExpressionEvaluator { return jsCode; } - String _createJsLambdaWithTryCatch( - String expression, - Iterable params, - ) { - final args = params.join(', '); - return ' ' - ' function($args) {\n' - ' try {\n' - ' return $expression($args);\n' - ' } catch (error) {\n' - ' return error.name + ": " + error.message;\n' - ' }\n' - '} '; + /// Create JS expression to pass to `Debugger.evaluateOnCallFrame`. + static String _createEvalExpression(String expression) { + final body = expression.split('\n').where((e) => e.isNotEmpty); + + return JsBuilder.createEvalExpression(body); } - String _createTryCatch(String expression) => ' ' - ' try {\n' - ' $expression;\n' - ' } catch (error) {\n' - ' error.name + ": " + error.message;\n' - ' }\n'; + /// Create JS function to invoke in `Runtime.callFunctionOn`. + static String _createEvalFunction( + String function, + Iterable params, + ) { + final body = function.split('\n').where((e) => e.isNotEmpty); - String _createDartLambda(String expression, Iterable params) => - '(${params.join(', ')}) => $expression'; + return params.contains('this') + ? JsBuilder.createEvalBoundFunction(body, params) + : JsBuilder.createEvalStaticFunction(body, params); + } } diff --git a/dwds/lib/src/services/javascript_builder.dart b/dwds/lib/src/services/javascript_builder.dart new file mode 100644 index 000000000..0814a52d1 --- /dev/null +++ b/dwds/lib/src/services/javascript_builder.dart @@ -0,0 +1,273 @@ +// Copyright (c) 2023, 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. + +/// Efficient JavaScript code builder. +/// +/// Used to create wrapper expressions and functions for expression evaluation. +class JsBuilder { + var _indent = 0; + final _buffer = StringBuffer(); + + String? _built; + String build() => _built ??= _buffer.toString(); + + JsBuilder(); + + void write(String item) { + _buffer.write(item); + } + + void writeLine(String item) { + _buffer.writeln(item); + } + + void writeAll(Iterable items, [String separator = '']) { + _buffer.writeAll(items, separator); + } + + void _writeIndent() { + writeAll([for (var i = 0; i < _indent * 2; i++) ' '], ''); + } + + void writeWithIndent(String item) { + _writeIndent(); + write(item); + } + + void writeLineWithIndent(String line) { + _writeIndent(); + writeLine(line); + } + + void writeMultiLineExpression(Iterable lines) { + var i = 0; + for (var line in lines) { + if (i == 0) { + writeLine(line); + } else if (i < lines.length - 1) { + writeLineWithIndent(line); + } else { + writeWithIndent(line); + } + i++; + } + } + + void increaseIndent() { + _indent++; + } + + void decreaseIndent() { + if (_indent != 0) _indent--; + } + + /// Call the expression built by [build] with [args]. + /// + /// $function($args); + void writeCallExpression( + Iterable args, + void Function() build, + ) { + build(); + write('('); + writeAll(args, ', '); + write(')'); + } + + /// Wrap the expression built by [build] in try/catch block. + /// + /// try { + /// $expression; + /// } catch (error) { + /// error.name + ": " + error.message; + /// }; + void writeTryCatchExpression(void Function() build) { + writeLineWithIndent('try {'); + + increaseIndent(); + writeWithIndent(''); + build(); + writeLine(''); + decreaseIndent(); + + writeLineWithIndent('} catch (error) {'); + writeLineWithIndent(' error.name + ": " + error.message;'); + writeWithIndent('}'); + } + + /// Wrap the statement built by [build] in try/catch block. + /// + /// try { + /// $statement + /// } catch (error) { + /// return error.name + ": " + error.message; + /// }; + void writeTryCatchStatement(void Function() build) { + writeLineWithIndent('try {'); + + increaseIndent(); + build(); + writeLine(''); + decreaseIndent(); + + writeLineWithIndent('} catch (error) {'); + writeLineWithIndent(' return error.name + ": " + error.message;'); + writeWithIndent('}'); + } + + /// Return the expression built by [build]. + /// + /// return $expression; + void writeReturnStatement(void Function() build) { + writeWithIndent('return '); + build(); + write(';'); + } + + /// Define a function with [params] and body built by [build]. + /// + /// function($args) { + /// $body + /// }; + void writeFunctionDefinition( + Iterable params, + void Function() build, + ) { + write('function ('); + writeAll(params, ', '); + writeLine(') {'); + + increaseIndent(); + build(); + writeLine(''); + decreaseIndent(); + + writeWithIndent('}'); + } + + /// Bind the function built by [build] to [to]. + /// + /// $function.bind($to) + void writeBindExpression( + String to, + void Function() build, + ) { + build(); + write('.bind('); + write(to); + write(')'); + } + + /// Create a wrapper expression to evaluate the [body]. + /// + /// Can be used in `Debugger.evaluateOnCallFrame` Chrome API. + /// + /// try { + /// $expression; + /// } catch (error) { + /// error.name + ": " + error.message; + /// } + static String createEvalExpression(Iterable body) => + (JsBuilder().._writeEvalExpression(body)).build(); + + void _writeEvalExpression(Iterable body) { + writeTryCatchExpression(() { + writeMultiLineExpression(body); + write(';'); + }); + } + + /// Create a wrapper function with [params] that calls a static [function]. + /// + /// Can be used in `Runtime.callFunctionOn` Chrome API. + /// + /// function ($params) { + /// try { + /// return $function($params); + /// } catch (error) { + /// return error.name + ": " + error.message; + /// } + /// } + static String createEvalStaticFunction( + Iterable function, + Iterable params, + ) => + (JsBuilder().._writeEvalStaticFunction(function, params)).build(); + + void _writeEvalStaticFunction( + Iterable function, + Iterable params, + ) { + writeFunctionDefinition( + params, + () => writeTryCatchStatement( + () => writeReturnStatement( + () => writeCallExpression( + params, + () { + writeMultiLineExpression(function); + }, + ), + ), + ), + ); + } + + /// Create a wrapper function with [params] that calls a bound [function]. + /// + /// Can be used in `Runtime.callFunctionOn` Chrome API. + /// function ($params, __t$this) { + /// try { + /// return function ($params) { + /// return $function($params); + /// }.bind(__t$this)($params); + /// } catch (error) { + /// return error.name + ": " + error.message; + /// } + /// } + static String createEvalBoundFunction( + Iterable function, + Iterable params, + ) => + (JsBuilder().._writeEvalBoundFunction(function, params)).build(); + + void _writeEvalBoundFunction( + Iterable function, + Iterable params, + ) { + final original = 'this'; + final substitute = '__t\$this'; + + final args = params.where((e) => e != original); + final substitutedParams = [ + ...params.where((e) => e != original), + substitute + ]; + + writeFunctionDefinition( + substitutedParams, + () => writeTryCatchStatement( + () => writeReturnStatement( + () => writeCallExpression( + args, + () => writeBindExpression( + substitute, + () => writeFunctionDefinition( + args, + () => writeReturnStatement( + () => writeCallExpression( + args, + () { + writeMultiLineExpression(function); + }, + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/dwds/test/evaluate_common.dart b/dwds/test/evaluate_common.dart index 5f1633872..f8aa7b728 100644 --- a/dwds/test/evaluate_common.dart +++ b/dwds/test/evaluate_common.dart @@ -164,6 +164,117 @@ void testAll({ getInstance(InstanceRef ref) async => await context.service.getObject(isolateId, ref.id!) as Instance; + test('with scope', () async { + await onBreakPoint(mainScript, 'printFrame1', (event) async { + final frame = event.topFrame!.index!; + + final scope = { + 'x1': (await getInstanceRef(frame, '"cat"')).id!, + 'x2': (await getInstanceRef(frame, '2')).id!, + 'x3': (await getInstanceRef(frame, 'MainClass(1,0)')).id!, + }; + + final result = await getInstanceRef( + frame, + '"\$x1\$x2 (\$x3) \$testLibraryValue (\$local1)"', + scope: scope, + ); + + expect(result, matchInstanceRef('cat2 (1, 0) 3 (1)')); + }); + }); + + test('with large scope', () async { + await onBreakPoint(mainScript, 'printLocal', (event) async { + const N = 20; + final frame = event.topFrame!.index!; + + final scope = { + for (var i = 0; i < N; i++) + 'x$i': (await getInstanceRef(frame, '$i')).id!, + }; + final expression = [ + for (var i = 0; i < N; i++) '\$x$i', + ].join(' '); + final expected = [ + for (var i = 0; i < N; i++) '$i', + ].join(' '); + + final result = await evaluateInFrame( + frame, + '"$expression"', + scope: scope, + ); + expect(result, matchInstanceRef(expected)); + }); + }); + + test('with large code scope', () async { + await onBreakPoint(mainScript, 'printLargeScope', (event) async { + const xN = 2; + const tN = 20; + final frame = event.topFrame!.index!; + + final scope = { + for (var i = 0; i < xN; i++) + 'x$i': (await getInstanceRef(frame, '$i')).id!, + }; + final expression = [ + for (var i = 0; i < xN; i++) '\$x$i', + for (var i = 0; i < tN; i++) '\$t$i', + ].join(' '); + final expected = [ + for (var i = 0; i < xN; i++) '$i', + for (var i = 0; i < tN; i++) '$i', + ].join(' '); + + final result = await evaluateInFrame( + frame, + '"$expression"', + scope: scope, + ); + expect(result, matchInstanceRef(expected)); + }); + }); + + test('with scope in caller frame', () async { + await onBreakPoint(mainScript, 'printFrame1', (event) async { + final frame = event.topFrame!.index! + 1; + + final scope = { + 'x1': (await getInstanceRef(frame, '"cat"')).id!, + 'x2': (await getInstanceRef(frame, '2')).id!, + 'x3': (await getInstanceRef(frame, 'MainClass(1,0)')).id!, + }; + + final result = await getInstanceRef( + frame, + '"\$x1\$x2 (\$x3) \$testLibraryValue (\$local2)"', + scope: scope, + ); + + expect(result, matchInstanceRef('cat2 (1, 0) 3 (2)')); + }); + }); + + test('with scope and this', () async { + await onBreakPoint(mainScript, 'toStringMainClass', (event) async { + final frame = event.topFrame!.index!; + + final scope = { + 'x1': (await getInstanceRef(frame, '"cat"')).id!, + }; + + final result = await getInstanceRef( + frame, + '"\$x1 \${this._field} \${this.field}"', + scope: scope, + ); + + expect(result, matchInstanceRef('cat 1 2')); + }); + }); + test( 'extension method scope variables can be evaluated', () async { @@ -173,7 +284,8 @@ void testAll({ for (var p in scope.entries) { final name = p.key; final value = p.value as InstanceRef; - final result = getInstanceRef(event.topFrame!.index!, name!); + final result = + await getInstanceRef(event.topFrame!.index!, name!); expect(result, matchInstanceRef(value.valueAsString)); } @@ -576,6 +688,47 @@ void testAll({ return isolate.rootLib!.id!; } + test('with scope', () async { + final libraryId = getRootLibraryId(); + + final scope = { + 'x1': (await getInstanceRef(libraryId, '"cat"')).id!, + 'x2': (await getInstanceRef(libraryId, '2')).id!, + 'x3': (await getInstanceRef(libraryId, 'MainClass(1,0)')).id!, + }; + + final result = await getInstanceRef( + libraryId, + '"\$x1\$x2 (\$x3) \$testLibraryValue"', + scope: scope, + ); + + expect(result, matchInstanceRef('cat2 (1, 0) 3')); + }); + + test('with large scope', () async { + final libraryId = getRootLibraryId(); + const N = 2; + + final scope = { + for (var i = 0; i < N; i++) + 'x$i': (await getInstanceRef(libraryId, '$i')).id!, + }; + final expression = [ + for (var i = 0; i < N; i++) '\$x$i', + ].join(' '); + final expected = [ + for (var i = 0; i < N; i++) '$i', + ].join(' '); + + final result = await getInstanceRef( + libraryId, + '"$expression"', + scope: scope, + ); + expect(result, matchInstanceRef(expected)); + }); + test('in parallel (in a batch)', () async { final libraryId = getRootLibraryId(); diff --git a/dwds/test/expression_evaluator_test.dart b/dwds/test/expression_evaluator_test.dart index e934d8366..e8875e48c 100644 --- a/dwds/test/expression_evaluator_test.dart +++ b/dwds/test/expression_evaluator_test.dart @@ -156,24 +156,6 @@ void main() async { ); }); - test('cannot evaluate expression in frame with non-empty scope', - () async { - final result = await evaluator - .evaluateExpressionInFrame('1', 0, 'true', {'a': '1'}); - expect( - result, - const TypeMatcher() - .having((o) => o.type, 'type', 'InternalError') - .having( - (o) => o.value, - 'value', - contains( - 'Using scope for expression evaluation in frame is not supported', - ), - ), - ); - }); - test('returns error if closed', () async { evaluator.close(); final result = diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index f29b97c31..53c165da2 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -279,7 +279,7 @@ class TestContext { break; case CompilationMode.frontendServer: { - _logger.warning('Index: $project.filePathToServe'); + _logger.info('Index: ${project.filePathToServe}'); final entry = p.toUri( p.join(project.webAssetsPath, project.dartEntryFileName), diff --git a/dwds/test/javascript_builder_test.dart b/dwds/test/javascript_builder_test.dart new file mode 100644 index 000000000..41fd8fa63 --- /dev/null +++ b/dwds/test/javascript_builder_test.dart @@ -0,0 +1,193 @@ +// Copyright (c) 2023, 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. + +@TestOn('vm') +@Timeout(Duration(minutes: 2)) + +import 'package:dwds/src/services/javascript_builder.dart'; +import 'package:test/test.dart'; + +void main() async { + group('JavaScriptBuilder |', () { + test('write', () async { + expect((JsBuilder()..write('Hello')).build(), 'Hello'); + }); + + test('writeLine', () async { + expect((JsBuilder()..writeLine('Hello')).build(), 'Hello\n'); + }); + + test('writeAll with separator', () async { + expect( + (JsBuilder()..writeAll(['Hello', 'World'], ' ')).build(), + 'Hello World', + ); + }); + + test('writeAll with default separator', () async { + expect( + (JsBuilder()..writeAll(['Hello', 'World'])).build(), + 'HelloWorld', + ); + }); + + test('writeWithIndent', () async { + expect( + (JsBuilder()..writeWithIndent('Hello')).build(), + 'Hello', + ); + }); + + test('writeWithIndent', () async { + final jsBuilder = JsBuilder(); + jsBuilder.increaseIndent(); + jsBuilder.writeWithIndent('Hello'); + jsBuilder.decreaseIndent(); + jsBuilder.writeWithIndent('World'); + expect(jsBuilder.build(), ' HelloWorld'); + }); + + test('writeLineWithIndent', () async { + final jsBuilder = JsBuilder(); + jsBuilder.increaseIndent(); + jsBuilder.writeLineWithIndent('Hello'); + jsBuilder.decreaseIndent(); + jsBuilder.writeLineWithIndent('World'); + expect(jsBuilder.build(), ' Hello\nWorld\n'); + }); + + test('writeAllLinesWithIndent', () async { + final jsBuilder = JsBuilder(); + jsBuilder.increaseIndent(); + jsBuilder.writeMultiLineExpression(['Hello', 'World']); + jsBuilder.decreaseIndent(); + jsBuilder.writeMultiLineExpression(['Hello', 'World']); + expect(jsBuilder.build(), 'Hello\n WorldHello\nWorld'); + }); + + test('writeCallExpression', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeCallExpression(['a1', 'a2'], () => jsBuilder.write('foo')); + expect(jsBuilder.build(), 'foo(a1, a2)'); + }); + + test('writeTryCatchExpression', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeTryCatchExpression(() => jsBuilder.write('x')); + expect( + jsBuilder.build(), + 'try {\n' + ' x\n' + '} catch (error) {\n' + ' error.name + ": " + error.message;\n' + '}'); + }); + + test('writeTryCatchStatement', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeTryCatchStatement( + () => jsBuilder.writeReturnStatement( + () => jsBuilder.write('x'), + ), + ); + expect( + jsBuilder.build(), + 'try {\n' + ' return x;\n' + '} catch (error) {\n' + ' return error.name + ": " + error.message;\n' + '}'); + }); + + test('writeReturnStatement', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeReturnStatement(() => jsBuilder.write('x')); + expect(jsBuilder.build(), 'return x;'); + }); + + test('writeFunctionDefinition', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeFunctionDefinition( + ['a1', 'a2'], + () => jsBuilder.writeReturnStatement( + () => jsBuilder.write('a1 + a2'), + ), + ); + expect( + jsBuilder.build(), + 'function (a1, a2) {\n' + ' return a1 + a2;\n' + '}'); + }); + + test('writeBindExpression', () async { + final jsBuilder = JsBuilder(); + jsBuilder.writeBindExpression( + 'x', + () => jsBuilder.writeFunctionDefinition( + [], + () => jsBuilder.writeReturnStatement( + () => jsBuilder.write('this.a'), + ), + ), + ); + expect( + jsBuilder.build(), + 'function () {\n' + ' return this.a;\n' + '}.bind(x)'); + }); + + test('createEvalExpression', () async { + final expression = + JsBuilder.createEvalExpression(['var e = 1;', 'return e']); + expect( + expression, + 'try {\n' + ' var e = 1;\n' + ' return e;\n' + '} catch (error) {\n' + ' error.name + ": " + error.message;\n' + '}'); + }); + + test('createEvalStaticFunction', () async { + final function = JsBuilder.createEvalStaticFunction( + ['function(e, e2) {', ' return e;', '}'], + ['e', 'e2'], + ); + expect( + function, + 'function (e, e2) {\n' + ' try {\n' + ' return function(e, e2) {\n' + ' return e;\n' + ' }(e, e2);\n' + ' } catch (error) {\n' + ' return error.name + ": " + error.message;\n' + ' }\n' + '}'); + }); + + test('createEvalBoundFunction', () async { + final function = JsBuilder.createEvalBoundFunction( + ['function(e, e2) {', ' return e;', '}'], + ['e', 'e2'], + ); + expect( + function, + 'function (e, e2, __t\$this) {\n' + ' try {\n' + ' return function (e, e2) {\n' + ' return function(e, e2) {\n' + ' return e;\n' + ' }(e, e2);\n' + ' }.bind(__t\$this)(e, e2);\n' + ' } catch (error) {\n' + ' return error.name + ": " + error.message;\n' + ' }\n' + '}'); + }); + }); +} diff --git a/fixtures/_testPackage/web/main.dart b/fixtures/_testPackage/web/main.dart index 8597df21c..b52882d62 100644 --- a/fixtures/_testPackage/web/main.dart +++ b/fixtures/_testPackage/web/main.dart @@ -50,6 +50,8 @@ void main() { printList(); printMap(); printSet(); + printFrame2(); + printLargeScope(); // For testing evaluation in async JS frames. registerUserExtension(extensionId++); }); @@ -163,13 +165,50 @@ ClassWithMethod createObject() { return ClassWithMethod(0); // Breakpoint: createObjectWithMethod } +void printFrame2() { + final local2 = 2; + print(local2); + printFrame1(); +} + +void printFrame1() { + final local1 = 1; + print(local1); // Breakpoint: printFrame1 +} + +void printLargeScope() { + var t0 = 0; + var t1 = 1; + var t2 = 2; + var t3 = 3; + var t4 = 4; + var t5 = 5; + var t6 = 6; + var t7 = 7; + var t8 = 8; + var t9 = 9; + var t10 = 10; + var t11 = 11; + var t12 = 12; + var t13 = 13; + var t14 = 14; + var t15 = 15; + var t16 = 16; + var t17 = 17; + var t18 = 18; + var t19 = 19; + + print('$t0 $t1, $t2, $t3, $t4, $t5, $t6, $t7, $t8, $t9, $t10, ' + '$t11, $t12, $t13, $t14, $t15, $t16, $t17, $t18, $t19'); // Breakpoint: printLargeScope +} + class MainClass { final int field; final int _field; MainClass(this.field, this._field); // Breakpoint: newMainClass @override - String toString() => '$field, $_field'; + String toString() => '$field, $_field'; // Breakpoint: toStringMainClass } class EnclosedClass { diff --git a/fixtures/_testPackageSound/web/main.dart b/fixtures/_testPackageSound/web/main.dart index b75aa9275..63da83412 100644 --- a/fixtures/_testPackageSound/web/main.dart +++ b/fixtures/_testPackageSound/web/main.dart @@ -49,6 +49,8 @@ void main() async { printList(); printMap(); printSet(); + printFrame2(); + printLargeScope(); // For testing evaluation in async JS frames. registerUserExtension(extensionId++); }); @@ -171,13 +173,50 @@ ClassWithMethod createObject() { return ClassWithMethod(0); // Breakpoint: createObjectWithMethod } +void printFrame2() { + final local2 = 2; + print(local2); + printFrame1(); +} + +void printFrame1() { + final local1 = 1; + print(local1); // Breakpoint: printFrame1 +} + +void printLargeScope() { + var t0 = 0; + var t1 = 1; + var t2 = 2; + var t3 = 3; + var t4 = 4; + var t5 = 5; + var t6 = 6; + var t7 = 7; + var t8 = 8; + var t9 = 9; + var t10 = 10; + var t11 = 11; + var t12 = 12; + var t13 = 13; + var t14 = 14; + var t15 = 15; + var t16 = 16; + var t17 = 17; + var t18 = 18; + var t19 = 19; + + print('$t0 $t1, $t2, $t3, $t4, $t5, $t6, $t7, $t8, $t9, $t10, ' + '$t11, $t12, $t13, $t14, $t15, $t16, $t17, $t18, $t19'); // Breakpoint: printLargeScope +} + class MainClass { final int field; final int _field; MainClass(this.field, this._field); // Breakpoint: newMainClass @override - String toString() => '$field, $_field'; + String toString() => '$field, $_field'; // Breakpoint: toStringMainClass } class EnclosedClass {