| // 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. |
| |
| 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'; |
| import 'package:vm/frontend_server.dart' show ProgramTransformer; |
| |
| // 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 = |
| 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>[ |
| NamedExpression('line', IntLiteral(location.line)), |
| NamedExpression('column', IntLiteral(location.column)), |
| ]; |
| if (showFile) { |
| arguments.add(NamedExpression( |
| 'file', StringLiteral(location.file.toString()))); |
| } |
| if (name != null) { |
| arguments.add(NamedExpression('name', StringLiteral(name))); |
| } |
| if (parameterLocations != null) { |
| arguments |
| .add(NamedExpression('parameterLocations', parameterLocations)); |
| } |
| return ConstructorInvocation( |
| _locationClass.constructors.first, |
| 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 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: 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 implements ProgramTransformer { |
| 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. |
| ClassHierarchy 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(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 = Field( |
| Name( |
| _locationFieldName, |
| _hasCreationLocationClass.enclosingLibrary, |
| ), |
| isFinal: true, |
| ); |
| clazz.addMember(locationField); |
| |
| final Set<Constructor> _handledConstructors = |
| Set<Constructor>.identity(); |
| |
| void handleConstructor(Constructor constructor) { |
| if (!_handledConstructors.add(constructor)) { |
| return; |
| } |
| assert(!_hasNamedParameter( |
| constructor.function, |
| _creationLocationParameterName, |
| )); |
| final VariableDeclaration variable = 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, |
| VariableGet(variable), |
| _locationClass, |
| ); |
| hasRedirectingInitializer = true; |
| break; |
| } |
| } |
| if (!hasRedirectingInitializer) { |
| constructor.initializers.add(FieldInitializer( |
| locationField, |
| 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(AssertInitializer(AssertStatement( |
| // IsExpression( |
| // VariableGet(variable), _locationClass.thisType), |
| // conditionStartOffset: constructor.fileOffset, |
| // conditionEndOffset: constructor.fileOffset, |
| // ))); |
| } |
| } |
| |
| // Add named parameters to all constructors. |
| clazz.constructors.forEach(handleConstructor); |
| } |
| |
| Component _computeFullProgram(Component deltaProgram) { |
| final Set<Library> libraries = <Library>{}; |
| final List<Library> workList = <Library>[]; |
| for (Library library in deltaProgram.libraries) { |
| if (libraries.add(library)) { |
| workList.add(library); |
| } |
| } |
| while (workList.isNotEmpty) { |
| final Library library = workList.removeLast(); |
| for (LibraryDependency dependency in library.dependencies) { |
| if (libraries.add(dependency.targetLibrary)) { |
| workList.add(dependency.targetLibrary); |
| } |
| } |
| } |
| return Component()..libraries.addAll(libraries); |
| } |
| |
| /// Transform the given [program]. |
| /// |
| /// It is safe to call this method on a delta program generated as part of |
| /// performing a hot reload. |
| @override |
| void transform(Component program) { |
| final List<Library> libraries = program.libraries; |
| |
| if (libraries.isEmpty) { |
| return; |
| } |
| |
| _resolveFlutterClasses(libraries); |
| |
| if (_widgetClass == null) { |
| // This application doesn't actually use the package:flutter library. |
| return; |
| } |
| |
| // TODO(jacobr): once there is a working incremental ClassHierarchy |
| // constructor switch to using it instead of building a ClassHierarchy off |
| // the full program. |
| hierarchy = ClassHierarchy( |
| _computeFullProgram(program), |
| onAmbiguousSupertypes: (Class cls, Supertype a, Supertype b) { }, |
| ); |
| |
| final Set<Class> transformedClasses = Set<Class>.identity(); |
| final Set<Library> librariesToTransform = Set<Library>.identity() |
| ..addAll(libraries); |
| |
| for (Library library in 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 = |
| _WidgetCallSiteTransformer( |
| hierarchy, |
| widgetClass: _widgetClass, |
| locationClass: _locationClass, |
| ); |
| |
| for (Library library in 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, |
| 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 = |
| Set<Constructor>.identity(); |
| |
| void handleConstructor(Constructor constructor) { |
| if (!_handledConstructors.add(constructor)) { |
| return; |
| } |
| |
| final VariableDeclaration variable = 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, |
| VariableGet(variable), |
| _locationClass, |
| ); |
| } else if (initializer is SuperInitializer && |
| _isSubclassOfWidget(initializer.target.enclosingClass)) { |
| _maybeAddCreationLocationArgument( |
| initializer.arguments, |
| initializer.target.function, |
| VariableGet(variable), |
| _locationClass, |
| ); |
| } |
| } |
| } |
| |
| clazz.constructors.forEach(handleConstructor); |
| } |
| } |