| // 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. |
| |
| import 'package:dwds/src/utilities/domain.dart'; |
| import 'package:dwds/src/utilities/objects.dart'; |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; |
| |
| /// The regular expressions used to filter out temp variables. |
| /// Needs to be kept in sync with SDK repo. |
| /// |
| /// TODO(annagrin) - use an alternative way to identify |
| /// synthetic variables. |
| /// Issue: https://github.com/dart-lang/sdk/issues/44262 |
| final ddcTemporaryVariableRegExp = RegExp( |
| // Starts with t$ |
| r'^t\$' |
| // followed by anything |
| r'.*' |
| // or, |
| r'|' |
| // anything that contains the sequence '$35'. |
| r'.*\$35.*', |
| ); |
| final ddcTemporaryTypeVariableRegExp = RegExp(r'^__t[\$\w*]+$'); |
| |
| /// Temporary variable regex before SDK changes for patterns. |
| /// TODO(annagrin): remove after dart 3.0 is stable. |
| 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]. |
| /// |
| /// See chromedevtools.github.io/devtools-protocol/tot/Debugger#type-CallFrame. |
| Future<List<Property>> visibleVariables({ |
| required AppInspectorInterface inspector, |
| required WipCallFrame frame, |
| }) async { |
| final allProperties = <Property>[]; |
| |
| if (frame.thisObject.type != 'undefined') { |
| allProperties.add(Property({'name': 'this', 'value': frame.thisObject})); |
| } |
| |
| // TODO: Try and populate all the property info for the scopes in one backend |
| // call. Along with some other optimizations (caching classRef lookups), we'd |
| // end up averaging one backend call per frame. |
| |
| // Iterate to least specific scope last to help preserve order in the local |
| // variables view when stepping. |
| for (final scope in filterScopes(frame).reversed) { |
| final objectId = scope.object.objectId; |
| if (objectId != null) { |
| final properties = await inspector.getProperties(objectId); |
| allProperties.addAll(properties); |
| } |
| } |
| |
| if (frame.returnValue != null && frame.returnValue!.type != 'undefined') { |
| allProperties.add(Property({'name': 'return', 'value': frame.returnValue})); |
| } |
| |
| // 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; |
| |
| final type = value.type; |
| if (type == 'undefined') return true; |
| |
| final description = value.description ?? ''; |
| final name = property.name ?? ''; |
| |
| // TODO(#786) Handle these correctly rather than just suppressing them. |
| // We should never see a raw JS class. The only case where this happens is a |
| // Dart generic function, where the type arguments get passed in as |
| // parameters. Hide those. |
| return (type == 'function' && description.startsWith('class ')) || |
| previousDdcTemporaryVariableRegExp.hasMatch(name) || |
| ddcTemporaryVariableRegExp.hasMatch(name) || |
| ddcTemporaryTypeVariableRegExp.hasMatch(name) || |
| (type == 'object' && description == 'dart.LegacyType.new'); |
| }); |
| |
| return allProperties; |
| } |
| |
| /// Filters the provided frame scopes to those that are pertinent for Dart |
| /// debugging. |
| List<WipScope> filterScopes(WipCallFrame frame) { |
| final scopes = frame.getScopeChain().toList(); |
| // Remove outer scopes up to and including the Dart SDK. |
| while (scopes.isNotEmpty && |
| !(scopes.last.name?.startsWith('load__') ?? false)) { |
| scopes.removeLast(); |
| } |
| if (scopes.isNotEmpty) scopes.removeLast(); |
| return scopes; |
| } |