diff --git a/dwds/lib/src/debugging/dart_scope.dart b/dwds/lib/src/debugging/dart_scope.dart index 5e340a65a..6702c9e1c 100644 --- a/dwds/lib/src/debugging/dart_scope.dart +++ b/dwds/lib/src/debugging/dart_scope.dart @@ -28,6 +28,9 @@ final ddcTemporaryTypeVariableRegExp = RegExp(r'^__t[\$\w*]+$'); final previousDdcTemporaryVariableRegExp = RegExp(r'^(t[0-9]+\$?[0-9]*|__t[\$\w*]+)$'); +const ddcAsyncScope = 'asyncScope'; +const ddcCapturedAsyncScope = 'capturedAsyncScope'; + /// Find the visible Dart variables from a JS Scope Chain, coming from the /// scopeChain attribute of a Chrome CallFrame corresponding to [frame]. /// @@ -70,6 +73,46 @@ Future> visibleVariables({ ); } + // DDC's async lowering hoists variable declarations into scope objects. We + // create one scope object per Dart scope (skipping scopes containing no + // declarations). If a Dart scope is captured by a Dart closure the + // JS scope object will also be captured by the compiled JS closure. + // + // For debugging purposes we unpack these scope objects into the set of + // available properties to recreate the Dart context at any given point. + + final capturedAsyncScopes = [ + ...allProperties + .where((p) => p.name?.startsWith(ddcCapturedAsyncScope) ?? false), + ]; + + if (capturedAsyncScopes.isNotEmpty) { + // If we are in a local function within an async function, we should use the + // available captured scopes. These will contain all the variables captured + // by the closure. We only close over variables used within the closure. + for (final scopeObject in capturedAsyncScopes) { + final scopeObjectId = scopeObject.value?.objectId; + if (scopeObjectId == null) continue; + final scopeProperties = await inspector.getProperties(scopeObjectId); + allProperties.addAll(scopeProperties); + allProperties.remove(scopeObject); + } + } else { + // Otherwise we are in the async function body itself. Unpack the available + // async scopes. Scopes we have not entered may already have a scope object + // declared but the object will not have any values in it yet. + final asyncScopes = [ + ...allProperties.where((p) => p.name?.startsWith(ddcAsyncScope) ?? false), + ]; + for (final scopeObject in asyncScopes) { + final scopeObjectId = scopeObject.value?.objectId; + if (scopeObjectId == null) continue; + final scopeProperties = await inspector.getProperties(scopeObjectId); + allProperties.addAll(scopeProperties); + allProperties.remove(scopeObject); + } + } + allProperties.removeWhere((property) { final value = property.value; if (value == null) return true; diff --git a/dwds/test/variable_scope_test.dart b/dwds/test/variable_scope_test.dart index bb0f18763..b3aabbb7b 100644 --- a/dwds/test/variable_scope_test.dart +++ b/dwds/test/variable_scope_test.dart @@ -187,30 +187,64 @@ void main() { expect(variableNames, containsAll(['formal'])); }); - test( - 'variables in function', - () async { - stack = await breakAt('nestedFunction', mainScript); - final variables = getFrameVariables(stack.frames!.first); - await expectDartVariables(variables); - - final variableNames = variables.keys.toList()..sort(); - expect( - variableNames, - containsAll([ - 'aClass', - 'another', - 'intLocalInMain', - 'local', - 'localThatsNull', - 'nestedFunction', - 'parameter', - 'testClass', - ]), - ); - }, - skip: 'See https://github.com/dart-lang/webdev/issues/2469', - ); + test('variables in static async function', () async { + stack = await breakAt('staticAsyncFunction', mainScript); + final variables = getFrameVariables(stack.frames!.first); + await expectDartVariables(variables); + + final variableNames = variables.keys.toList()..sort(); + final variableValues = + variableNames.map((name) => variables[name]?.valueAsString).toList(); + expect( + variableNames, + containsAll(['myLocal', 'value']), + ); + expect( + variableValues, + containsAll(['a local value', 'arg1']), + ); + }); + + test('variables in static async loop function', () async { + stack = await breakAt('staticAsyncLoopFunction', mainScript); + final variables = getFrameVariables(stack.frames!.first); + await expectDartVariables(variables); + + final variableNames = variables.keys.toList()..sort(); + final variableValues = + variableNames.map((name) => variables[name]?.valueAsString).toList(); + expect( + variableNames, + containsAll(['i', 'myLocal', 'value']), + ); + // Ensure the loop variable, i, is captued correctly. The value from the + // first iteration should be captured by the saved closure. + expect( + variableValues, + containsAll(['1', 'my local value', 'arg2']), + ); + }); + + test('variables in function', () async { + stack = await breakAt('nestedFunction', mainScript); + final variables = getFrameVariables(stack.frames!.first); + await expectDartVariables(variables); + + final variableNames = variables.keys.toList()..sort(); + expect( + variableNames, + containsAll([ + 'aClass', + 'another', + 'intLocalInMain', + 'local', + 'localThatsNull', + 'nestedFunction', + 'parameter', + 'testClass', + ]), + ); + }); test('variables in closure nested in method', () async { stack = await breakAt('nestedClosure', mainScript); diff --git a/fixtures/_testSound/example/scopes/main.dart b/fixtures/_testSound/example/scopes/main.dart index f553d75c1..899b043f2 100644 --- a/fixtures/_testSound/example/scopes/main.dart +++ b/fixtures/_testSound/example/scopes/main.dart @@ -25,6 +25,25 @@ void staticFunction(int formal) { print(formal); // Breakpoint: staticFunction } +void staticAsyncFunction(String value) async { + var myLocal = await 'a local value'; + print(value); // Breakpoint: staticAsyncFunction +} + +void staticAsyncLoopFunction(String value) async { + Function? f; + for (var i in [1, 2, 3]) { + print(i); + var myLocal = await 'my local value'; + f ??= () { + print(value); + print(i); + return myLocal; // Breakpoint: staticAsyncLoopFunction + }; + } + f!(); +} + void main() async { print('Initial print from scopes app'); var local = 'local in main'; @@ -52,6 +71,8 @@ void main() async { var closureLocal; libraryPublicFinal.printCount(); staticFunction(1); + staticAsyncFunction('arg1'); + staticAsyncLoopFunction('arg2'); print('ticking... $ticks (the answer is $intLocalInMain)'); print(nestedFunction('$ticks ${testClass.message}', Timer)); print(localThatsNull);