[dartdevc] fix #36174, add flutter inspector transform to DDC
This is a temporary workaround to copy this file into DDC, based on
jacobr@'s suggestion. I tried to keep changes minimal.
Eventually this transform should be injected when Flutter SDK builds
DDC, rather than including it here. The transform is guarded behind
a flag, so build runner can decide whether to pass it (for now, it
could be hardcoded).
Change-Id: Id9d36456a726e6abb46b904321bc31db136d1c2e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/97022
Commit-Queue: Jenny Messerly <jmesserly@google.com>
Reviewed-by: Jake Macdonald <jakemac@google.com>
Reviewed-by: Jacob Richman <jacobr@google.com>
Auto-Submit: Jenny Messerly <jmesserly@google.com>
diff --git a/pkg/dev_compiler/lib/src/flutter/track_widget_constructor_locations.dart b/pkg/dev_compiler/lib/src/flutter/track_widget_constructor_locations.dart
new file mode 100644
index 0000000..e44eb59
--- /dev/null
+++ b/pkg/dev_compiler/lib/src/flutter/track_widget_constructor_locations.dart
@@ -0,0 +1,573 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(jmesserly): this file was copied from:
+// https://github.com/flutter/engine/blob/4b01d795feec3ba8231a397a4ec2759954d8216e/flutter_kernel_transformers/lib/track_widget_constructor_locations.dart
+//
+// Longer term, this transform should be injected by Flutter when they building
+// the Flutter-specific `dartdevc` script.
+//
+// The following modifications were made:
+// - remove "package:vm" dependency (only used for one interface)
+// - pass in the class hierarchy that DDC already has available.
+library track_widget_constructor_locations;
+
+// The kernel/src import below that requires lint `ignore_for_file`
+// is a temporary state of things until kernel team builds better api that would
+// replace api used below. This api was made private in an effort to discourage
+// further use.
+// ignore_for_file: implementation_imports
+import 'package:kernel/ast.dart';
+import 'package:kernel/class_hierarchy.dart';
+import 'package:meta/meta.dart';
+
+// Parameter name used to track were widget constructor calls were made from.
+//
+// The parameter name contains a randomly generate hex string to avoid collision
+// with user generated parameters.
+const String _creationLocationParameterName =
+ r'$creationLocationd_0dea112b090073317d4';
+
+/// Name of private field added to the Widget class and any other classes that
+/// implement Widget.
+///
+/// Regardless of what library a class implementing Widget is defined in, the
+/// private field will always be defined in the context of the widget_inspector
+/// library ensuring no name conflicts with regular fields.
+const String _locationFieldName = r'_location';
+
+bool _hasNamedParameter(FunctionNode function, String name) {
+ return function.namedParameters
+ .any((VariableDeclaration parameter) => parameter.name == name);
+}
+
+bool _hasNamedArgument(Arguments arguments, String argumentName) {
+ return arguments.named
+ .any((NamedExpression argument) => argument.name == argumentName);
+}
+
+VariableDeclaration _getNamedParameter(
+ FunctionNode function,
+ String parameterName,
+) {
+ for (VariableDeclaration parameter in function.namedParameters) {
+ if (parameter.name == parameterName) {
+ return parameter;
+ }
+ }
+ return null;
+}
+
+// TODO(jacobr): find a solution that supports optional positional parameters.
+/// Add the creation location to the arguments list if possible.
+///
+/// Returns whether the creation location argument could be added. We cannot
+/// currently add the named argument for functions with optional positional
+/// parameters as the current scheme requires adding the creation location as a
+/// named parameter. Fortunately that is not a significant issue in practice as
+/// no Widget classes in package:flutter have optional positional parameters.
+/// This code degrades gracefully for constructors with optional positional
+/// parameters by skipping adding the creation location argument rather than
+/// failing.
+void _maybeAddCreationLocationArgument(
+ Arguments arguments,
+ FunctionNode function,
+ Expression creationLocation,
+ Class locationClass,
+) {
+ if (_hasNamedArgument(arguments, _creationLocationParameterName)) {
+ return;
+ }
+ if (!_hasNamedParameter(function, _creationLocationParameterName)) {
+ return;
+ }
+
+ final NamedExpression namedArgument =
+ new NamedExpression(_creationLocationParameterName, creationLocation);
+ namedArgument.parent = arguments;
+ arguments.named.add(namedArgument);
+}
+
+/// Adds a named parameter to a function if the function does not already have
+/// a named parameter with the name or optional positional parameters.
+bool _maybeAddNamedParameter(
+ FunctionNode function,
+ VariableDeclaration variable,
+) {
+ if (_hasNamedParameter(function, _creationLocationParameterName)) {
+ // Gracefully handle if this method is called on a function that has already
+ // been transformed.
+ return false;
+ }
+ // Function has optional positional parameters so cannot have optional named
+ // parameters.
+ if (function.requiredParameterCount != function.positionalParameters.length) {
+ return false;
+ }
+ variable.parent = function;
+ function.namedParameters.add(variable);
+ return true;
+}
+
+/// Transformer that modifies all calls to Widget constructors to include
+/// a [DebugLocation] parameter specifying the location where the constructor
+/// call was made.
+///
+/// This transformer requires that all Widget constructors have already been
+/// transformed to have a named parameter with the name specified by
+/// `_locationParameterName`.
+class _WidgetCallSiteTransformer extends Transformer {
+ final ClassHierarchy _hierarchy;
+
+ /// The [Widget] class defined in the `package:flutter` library.
+ ///
+ /// Used to perform instanceof checks to determine whether Dart constructor
+ /// calls are creating [Widget] objects.
+ Class _widgetClass;
+
+ /// The [DebugLocation] class defined in the `package:flutter` library.
+ Class _locationClass;
+
+ /// Current factory constructor that node being transformed is inside.
+ ///
+ /// Used to flow the location passed in as an argument to the factory to the
+ /// actual constructor call within the factory.
+ Procedure _currentFactory;
+
+ _WidgetCallSiteTransformer(
+ this._hierarchy, {
+ @required Class widgetClass,
+ @required Class locationClass,
+ }) : _widgetClass = widgetClass,
+ _locationClass = locationClass;
+
+ /// Builds a call to the const constructor of the [DebugLocation]
+ /// object specifying the location where a constructor call was made and
+ /// optionally the locations for all parameters passed in.
+ ///
+ /// Specifying the parameters passed in is an experimental feature. With
+ /// access to the source code of an application you could determine the
+ /// locations of the parameters passed in from the source location of the
+ /// constructor call but it is convenient to bundle the location and names
+ /// of the parameters passed in so that tools can show parameter locations
+ /// without re-parsing the source code.
+ ConstructorInvocation _constructLocation(
+ Location location, {
+ String name,
+ ListLiteral parameterLocations,
+ bool showFile: true,
+ }) {
+ final List<NamedExpression> arguments = <NamedExpression>[
+ new NamedExpression('line', new IntLiteral(location.line)),
+ new NamedExpression('column', new IntLiteral(location.column)),
+ ];
+ if (showFile) {
+ arguments.add(new NamedExpression(
+ 'file', new StringLiteral(location.file.toString())));
+ }
+ if (name != null) {
+ arguments.add(new NamedExpression('name', new StringLiteral(name)));
+ }
+ if (parameterLocations != null) {
+ arguments
+ .add(new NamedExpression('parameterLocations', parameterLocations));
+ }
+ return new ConstructorInvocation(
+ _locationClass.constructors.first,
+ new Arguments(<Expression>[], named: arguments),
+ isConst: true,
+ );
+ }
+
+ @override
+ Procedure visitProcedure(Procedure node) {
+ if (node.isFactory) {
+ _currentFactory = node;
+ node.transformChildren(this);
+ _currentFactory = null;
+ return node;
+ }
+ return defaultTreeNode(node);
+ }
+
+ bool _isSubclassOfWidget(Class clazz) {
+ // TODO(jacobr): use hierarchy.isSubclassOf once we are using the
+ // non-deprecated ClassHierarchy constructor.
+ return _hierarchy.isSubclassOf(clazz, _widgetClass);
+ }
+
+ @override
+ StaticInvocation visitStaticInvocation(StaticInvocation node) {
+ node.transformChildren(this);
+ final Procedure target = node.target;
+ if (!target.isFactory) {
+ return node;
+ }
+ final Class constructedClass = target.enclosingClass;
+ if (!_isSubclassOfWidget(constructedClass)) {
+ return node;
+ }
+
+ _addLocationArgument(node, target.function, constructedClass);
+ return node;
+ }
+
+ void _addLocationArgument(InvocationExpression node, FunctionNode function,
+ Class constructedClass) {
+ _maybeAddCreationLocationArgument(
+ node.arguments,
+ function,
+ _computeLocation(node, function, constructedClass),
+ _locationClass,
+ );
+ }
+
+ @override
+ ConstructorInvocation visitConstructorInvocation(ConstructorInvocation node) {
+ node.transformChildren(this);
+
+ final Constructor constructor = node.target;
+ final Class constructedClass = constructor.enclosingClass;
+ if (!_isSubclassOfWidget(constructedClass)) {
+ return node;
+ }
+
+ _addLocationArgument(node, constructor.function, constructedClass);
+ return node;
+ }
+
+ Expression _computeLocation(InvocationExpression node, FunctionNode function,
+ Class constructedClass) {
+ // For factory constructors we need to use the location specified as an
+ // argument to the factory constructor rather than the location
+ // TODO(jacobr): use hierarchy.isSubclassOf once we are using the
+ // non-deprecated ClassHierarchy constructor.
+ if (_currentFactory != null &&
+ _hierarchy.isSubclassOf(
+ constructedClass, _currentFactory.enclosingClass)) {
+ final VariableDeclaration creationLocationParameter = _getNamedParameter(
+ _currentFactory.function,
+ _creationLocationParameterName,
+ );
+ if (creationLocationParameter != null) {
+ return new VariableGet(creationLocationParameter);
+ }
+ }
+
+ final Arguments arguments = node.arguments;
+ final Location location = node.location;
+ final List<ConstructorInvocation> parameterLocations =
+ <ConstructorInvocation>[];
+ final List<VariableDeclaration> parameters = function.positionalParameters;
+ for (int i = 0; i < arguments.positional.length; ++i) {
+ final Expression expression = arguments.positional[i];
+ final VariableDeclaration parameter = parameters[i];
+ parameterLocations.add(_constructLocation(
+ expression.location,
+ name: parameter.name,
+ showFile: false,
+ ));
+ }
+ for (NamedExpression expression in arguments.named) {
+ parameterLocations.add(_constructLocation(
+ expression.location,
+ name: expression.name,
+ showFile: false,
+ ));
+ }
+ return _constructLocation(
+ location,
+ parameterLocations: new ListLiteral(
+ parameterLocations,
+ typeArgument: _locationClass.thisType,
+ isConst: true,
+ ),
+ );
+ }
+}
+
+/// Rewrites all widget constructors and constructor invocations to add a
+/// parameter specifying the location the constructor was called from.
+///
+/// The creation location is stored as a private field named `_location`
+/// on the base widget class and flowed through the constructors using a named
+/// parameter.
+class WidgetCreatorTracker {
+ Class _widgetClass;
+ Class _locationClass;
+
+ /// Marker interface indicating that a private _location field is
+ /// available.
+ Class _hasCreationLocationClass;
+
+ /// The [ClassHierarchy] that should be used after applying this transformer.
+ /// If any class was updated, in general we need to create a new
+ /// [ClassHierarchy] instance, with new dispatch targets; or at least let
+ /// the existing instance know that some of its dispatch tables are not
+ /// valid anymore.
+ final ClassHierarchy hierarchy;
+
+ WidgetCreatorTracker(this.hierarchy);
+
+ void _resolveFlutterClasses(Iterable<Library> libraries) {
+ // If the Widget or Debug location classes have been updated we need to get
+ // the latest version
+ for (Library library in libraries) {
+ final Uri importUri = library.importUri;
+ if (!library.isExternal &&
+ importUri != null &&
+ importUri.scheme == 'package') {
+ if (importUri.path == 'flutter/src/widgets/framework.dart') {
+ for (Class class_ in library.classes) {
+ if (class_.name == 'Widget') {
+ _widgetClass = class_;
+ }
+ }
+ } else {
+ if (importUri.path == 'flutter/src/widgets/widget_inspector.dart') {
+ for (Class class_ in library.classes) {
+ if (class_.name == '_HasCreationLocation') {
+ _hasCreationLocationClass = class_;
+ } else if (class_.name == '_Location') {
+ _locationClass = class_;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// Modify [clazz] to add a field named [_locationFieldName] that is the
+ /// first parameter of all constructors of the class.
+ ///
+ /// This method should only be called for classes that implement but do not
+ /// extend [Widget].
+ void _transformClassImplementingWidget(Class clazz) {
+ if (clazz.fields
+ .any((Field field) => field.name.name == _locationFieldName)) {
+ // This class has already been transformed. Skip
+ return;
+ }
+ clazz.implementedTypes
+ .add(new Supertype(_hasCreationLocationClass, <DartType>[]));
+ // We intentionally use the library context of the _HasCreationLocation
+ // class for the private field even if [clazz] is in a different library
+ // so that all classes implementing Widget behave consistently.
+ final Field locationField = new Field(
+ new Name(
+ _locationFieldName,
+ _hasCreationLocationClass.enclosingLibrary,
+ ),
+ isFinal: true,
+ );
+ clazz.addMember(locationField);
+
+ final Set<Constructor> _handledConstructors =
+ new Set<Constructor>.identity();
+
+ void handleConstructor(Constructor constructor) {
+ if (!_handledConstructors.add(constructor)) {
+ return;
+ }
+ assert(!_hasNamedParameter(
+ constructor.function,
+ _creationLocationParameterName,
+ ));
+ final VariableDeclaration variable = new VariableDeclaration(
+ _creationLocationParameterName,
+ type: _locationClass.thisType,
+ );
+ if (!_maybeAddNamedParameter(constructor.function, variable)) {
+ return;
+ }
+
+ bool hasRedirectingInitializer = false;
+ for (Initializer initializer in constructor.initializers) {
+ if (initializer is RedirectingInitializer) {
+ if (initializer.target.enclosingClass == clazz) {
+ // We need to handle this constructor first or the call to
+ // addDebugLocationArgument bellow will fail due to the named
+ // parameter not yet existing on the constructor.
+ handleConstructor(initializer.target);
+ }
+ _maybeAddCreationLocationArgument(
+ initializer.arguments,
+ initializer.target.function,
+ new VariableGet(variable),
+ _locationClass,
+ );
+ hasRedirectingInitializer = true;
+ break;
+ }
+ }
+ if (!hasRedirectingInitializer) {
+ constructor.initializers.add(new FieldInitializer(
+ locationField,
+ new VariableGet(variable),
+ ));
+ // TODO(jacobr): add an assert verifying the locationField is not
+ // null. Currently, we cannot safely add this assert because we do not
+ // handle Widget classes with optional positional arguments. There are
+ // no Widget classes in the flutter repo with optional positional
+ // arguments but it is possible users could add classes with optional
+ // positional arguments.
+ //
+ // constructor.initializers.add(new AssertInitializer(new AssertStatement(
+ // new IsExpression(
+ // new VariableGet(variable), _locationClass.thisType),
+ // conditionStartOffset: constructor.fileOffset,
+ // conditionEndOffset: constructor.fileOffset,
+ // )));
+ }
+ }
+
+ // Add named parameters to all constructors.
+ clazz.constructors.forEach(handleConstructor);
+ }
+
+ /// Transform the given [module].
+ void transform(Component module) {
+ final List<Library> libraries = module.libraries;
+
+ if (libraries.isEmpty) {
+ return;
+ }
+
+ _resolveFlutterClasses(libraries);
+
+ if (_widgetClass == null) {
+ // This application doesn't actually use the package:flutter library.
+ return;
+ }
+
+ final Set<Class> transformedClasses = new Set<Class>.identity();
+ final Set<Library> librariesToTransform = new Set<Library>.identity()
+ ..addAll(module.libraries);
+
+ for (Library library in module.libraries) {
+ if (library.isExternal) {
+ continue;
+ }
+ for (Class class_ in library.classes) {
+ _transformWidgetConstructors(
+ librariesToTransform,
+ transformedClasses,
+ class_,
+ );
+ }
+ }
+
+ // Transform call sites to pass the location parameter.
+ final _WidgetCallSiteTransformer callsiteTransformer =
+ new _WidgetCallSiteTransformer(
+ hierarchy,
+ widgetClass: _widgetClass,
+ locationClass: _locationClass,
+ );
+
+ for (Library library in module.libraries) {
+ if (library.isExternal) {
+ continue;
+ }
+ library.transformChildren(callsiteTransformer);
+ }
+ }
+
+ bool _isSubclassOfWidget(Class clazz) {
+ if (clazz == null) {
+ return false;
+ }
+ // TODO(jacobr): use hierarchy.isSubclassOf once we are using the
+ // non-deprecated ClassHierarchy constructor.
+ return hierarchy.isSubclassOf(clazz, _widgetClass);
+ }
+
+ void _transformWidgetConstructors(Set<Library> librariesToBeTransformed,
+ Set<Class> transformedClasses, Class clazz) {
+ if (!_isSubclassOfWidget(clazz) ||
+ !librariesToBeTransformed.contains(clazz.enclosingLibrary) ||
+ !transformedClasses.add(clazz)) {
+ return;
+ }
+
+ // Ensure super classes have been transformed before this class.
+ if (clazz.superclass != null &&
+ !transformedClasses.contains(clazz.superclass)) {
+ _transformWidgetConstructors(
+ librariesToBeTransformed,
+ transformedClasses,
+ clazz.superclass,
+ );
+ }
+
+ for (Procedure procedure in clazz.procedures) {
+ if (procedure.isFactory) {
+ _maybeAddNamedParameter(
+ procedure.function,
+ new VariableDeclaration(
+ _creationLocationParameterName,
+ type: _locationClass.thisType,
+ ),
+ );
+ }
+ }
+
+ // Handle the widget class and classes that implement but do not extend the
+ // widget class.
+ if (!_isSubclassOfWidget(clazz.superclass)) {
+ _transformClassImplementingWidget(clazz);
+ return;
+ }
+
+ final Set<Constructor> _handledConstructors =
+ new Set<Constructor>.identity();
+
+ void handleConstructor(Constructor constructor) {
+ if (!_handledConstructors.add(constructor)) {
+ return;
+ }
+
+ final VariableDeclaration variable = new VariableDeclaration(
+ _creationLocationParameterName,
+ type: _locationClass.thisType,
+ );
+ if (_hasNamedParameter(
+ constructor.function, _creationLocationParameterName)) {
+ // Constructor was already rewritten. TODO(jacobr): is this case actually hit?
+ return;
+ }
+ if (!_maybeAddNamedParameter(constructor.function, variable)) {
+ return;
+ }
+ for (Initializer initializer in constructor.initializers) {
+ if (initializer is RedirectingInitializer) {
+ if (initializer.target.enclosingClass == clazz) {
+ // We need to handle this constructor first or the call to
+ // addDebugLocationArgument could fail due to the named parameter
+ // not existing.
+ handleConstructor(initializer.target);
+ }
+
+ _maybeAddCreationLocationArgument(
+ initializer.arguments,
+ initializer.target.function,
+ new VariableGet(variable),
+ _locationClass,
+ );
+ } else if (initializer is SuperInitializer &&
+ _isSubclassOfWidget(initializer.target.enclosingClass)) {
+ _maybeAddCreationLocationArgument(
+ initializer.arguments,
+ initializer.target.function,
+ new VariableGet(variable),
+ _locationClass,
+ );
+ }
+ }
+ }
+
+ clazz.constructors.forEach(handleConstructor);
+ }
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/command.dart b/pkg/dev_compiler/lib/src/kernel/command.dart
index b3852c9..e7a764c 100644
--- a/pkg/dev_compiler/lib/src/kernel/command.dart
+++ b/pkg/dev_compiler/lib/src/kernel/command.dart
@@ -9,6 +9,7 @@
import 'package:args/args.dart';
import 'package:build_integration/file_system/multi_root.dart';
import 'package:cli_util/cli_util.dart' show getSdkPath;
+import 'package:dev_compiler/src/flutter/track_widget_constructor_locations.dart';
import 'package:front_end/src/api_unstable/ddc.dart' as fe;
import 'package:kernel/kernel.dart' hide MapEntry;
import 'package:kernel/text/ast_to_text.dart' as kernel show Printer;
@@ -72,6 +73,8 @@
help: 'emit API summary in a .js.txt file',
defaultsTo: false,
hide: true)
+ ..addFlag('track-widget-creation',
+ help: 'enable inspecting of Flutter widgets', hide: true)
// TODO(jmesserly): add verbose help to show hidden options
..addOption('dart-sdk-summary',
help: 'The path to the Dart SDK summary file.', hide: true)
@@ -252,8 +255,16 @@
outFiles.add(File(output + '.txt').writeAsString(sb.toString()));
}
var target = compilerState.options.target as DevCompilerTarget;
+ var hierarchy = target.hierarchy;
+
+ // TODO(jmesserly): remove this hack once Flutter SDK has a `dartdevc` with
+ // support for the widget inspector.
+ if (argResults['track-widget-creation'] as bool) {
+ WidgetCreatorTracker(hierarchy).transform(component);
+ }
+
var compiler =
- ProgramCompiler(component, target.hierarchy, options, declaredVariables);
+ ProgramCompiler(component, hierarchy, options, declaredVariables);
var jsModule = compiler.emitModule(component, result.inputSummaries,
compilerState.options.inputSummaries, summaryModules);