| // 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. | |
| library kernel.transformations.track_widget_constructor_locations; | |
| import '../ast.dart'; | |
| import '../target/changed_structure_notifier.dart'; | |
| // Parameter name used to track where widget constructor calls were made from. | |
| // | |
| // The parameter name contains a randomly generated 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( | |
| (NamedParameter parameter) => parameter.parameterName == name, | |
| ); | |
| } | |
| bool _hasNamedArgument(Arguments arguments, String argumentName) { | |
| return arguments.named.any( | |
| (NamedExpression argument) => argument.name == argumentName, | |
| ); | |
| } | |
| Variable? _getNamedParameter(FunctionNode function, String parameterName) { | |
| for (NamedParameter parameter in function.namedParameters) { | |
| if (parameter.parameterName == 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, | |
| ) { | |
| if (_hasNamedArgument(arguments, _creationLocationParameterName)) { | |
| return; | |
| } | |
| if (!_hasNamedParameter(function, _creationLocationParameterName)) { | |
| // TODO(jakemac): We don't apply the transformation to dependencies kernel | |
| // outlines, so instead we just assume the named parameter exists. | |
| // | |
| // The only case in which it shouldn't exist is if the function has optional | |
| // positional parameters so it cannot have optional named parameters. | |
| if (function.requiredParameterCount != | |
| function.positionalParameters.length) { | |
| return; | |
| } | |
| } | |
| final NamedExpression namedArgument = new NamedExpression( | |
| _creationLocationParameterName, | |
| creationLocation, | |
| ); | |
| namedArgument.parent = arguments; | |
| arguments.named = NamedExpressionList.generate( | |
| arguments.named.length + 1, | |
| (i) => i < arguments.named.length ? arguments.named[i] : 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, NamedParameter parameter) { | |
| 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; | |
| } | |
| parameter.parent = function; | |
| final NamedParameterList existing = function.namedParameters; | |
| function.namedParameters = NamedParameterList.generate( | |
| existing.length + 1, | |
| (int i) => i < existing.length ? existing[i] : parameter, | |
| ); | |
| return true; | |
| } | |
| /// Transformer that modifies all calls to Widget constructors and "Widget | |
| /// factories" to include a _Location parameter specifying the location where | |
| /// the constructor or widget factory call was made. | |
| /// | |
| /// This transformer requires that all Widget constructors and Widget factories | |
| /// have already been transformed to have a named parameter with the name | |
| /// specified by `_locationParameterName`. | |
| /// | |
| /// A "Widget factory" is an extension instance method annotated with the | |
| /// `@widgetFactory` annotations. A _Location parameter is added to such methods | |
| /// and this is used as the location value for all Widget constructor | |
| /// invocations within the method. | |
| class _WidgetCallSiteTransformer extends Transformer { | |
| final WidgetCreatorTracker _tracker; | |
| /// The creation location parameter of the extension factory method enclosing | |
| /// the node that is currently being transformed. | |
| /// | |
| /// Used to flow the creation location parameter to the call sites of widget | |
| /// constructors and extension factory methods within an enclosing extension | |
| /// factory method. | |
| Expression? _currentExtensionFactoryLocationParameter; | |
| /// 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; | |
| /// Library that contains the transformed call sites. | |
| /// | |
| /// The transformation of the call sites is affected by the NNBD opt-in status | |
| /// of the library. | |
| Library? _currentLibrary; | |
| new({required WidgetCreatorTracker tracker}) : _tracker = tracker; | |
| /// Builds a call to the const constructor of the _Location | |
| /// 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, { | |
| required Class locationClass, | |
| String? name, | |
| }) { | |
| final NamedExpressionList arguments = NamedExpressionList( | |
| new NamedExpression('file', new StringLiteral(location.file.toString())), | |
| new NamedExpression('line', new IntLiteral(location.line)), | |
| new NamedExpression('column', new IntLiteral(location.column)), | |
| name != null | |
| ? new NamedExpression('name', new StringLiteral(name)) | |
| : null, | |
| ); | |
| return new ConstructorInvocation( | |
| locationClass.constructors.first, | |
| new Arguments(ExpressionList.empty, named: arguments), | |
| isConst: true, | |
| ); | |
| } | |
| @override | |
| Procedure visitProcedure(Procedure node) { | |
| if (_isWidgetFactory(node)) { | |
| final NamedParameter locationParameter = node.function.namedParameters | |
| .firstWhere( | |
| (parameter) => | |
| parameter.parameterName == _creationLocationParameterName, | |
| ); | |
| _currentExtensionFactoryLocationParameter = VariableGet( | |
| locationParameter, | |
| ); | |
| node.transformChildren(this); | |
| _currentExtensionFactoryLocationParameter = null; | |
| return node; | |
| } | |
| if (node.isFactory) { | |
| _currentFactory = node; | |
| node.transformChildren(this); | |
| _currentFactory = null; | |
| return node; | |
| } | |
| node.transformChildren(this); | |
| return node; | |
| } | |
| bool _isWidgetFactory(Procedure node) { | |
| return node.isExtensionMember && | |
| _hasNamedParameter(node.function, _creationLocationParameterName); | |
| } | |
| @override | |
| StaticInvocation visitStaticInvocation(StaticInvocation node) { | |
| node.transformChildren(this); | |
| final Procedure target = node.target; | |
| if (target.isFactory) { | |
| final Class constructedClass = target.enclosingClass!; | |
| final _TrackingClasses? tracking = _tracker._getTrackingClasses( | |
| constructedClass, | |
| ); | |
| if (tracking == null) { | |
| return node; | |
| } | |
| _addLocationArgument( | |
| node, | |
| target.function, | |
| constructedClass: constructedClass, | |
| isConst: node.isConst, | |
| locationClass: tracking.locationClass, | |
| ); | |
| return node; | |
| } | |
| if (_isWidgetFactory(target)) { | |
| final NamedParameter parameter = target.function.namedParameters | |
| .firstWhere((p) => p.parameterName == _creationLocationParameterName); | |
| final DartType type = parameter.type; | |
| if (type is InterfaceType) { | |
| _addLocationArgument( | |
| node, | |
| target.function, | |
| locationClass: type.classNode, | |
| ); | |
| return node; | |
| } | |
| } | |
| return node; | |
| } | |
| void _addLocationArgument( | |
| InvocationExpression node, | |
| FunctionNode function, { | |
| Class? constructedClass, | |
| bool isConst = false, | |
| required Class locationClass, | |
| }) { | |
| Expression? location = _currentExtensionFactoryLocationParameter; | |
| if (location == null || | |
| // We cannot pass the location parameter of the enclosing extension | |
| // factory method to a const constructor call, so we fallback to | |
| // passing the location of the constructor call. | |
| isConst) { | |
| location = _computeLocation( | |
| node, | |
| function, | |
| constructedClass, | |
| isConst: isConst, | |
| locationClass: locationClass, | |
| ); | |
| } | |
| _maybeAddCreationLocationArgument(node.arguments, function, location); | |
| } | |
| @override | |
| ConstructorInvocation visitConstructorInvocation(ConstructorInvocation node) { | |
| node.transformChildren(this); | |
| final Constructor constructor = node.target; | |
| final Class constructedClass = constructor.enclosingClass; | |
| final _TrackingClasses? tracking = _tracker._getTrackingClasses( | |
| constructedClass, | |
| ); | |
| if (tracking == null) { | |
| return node; | |
| } | |
| _addLocationArgument( | |
| node, | |
| constructor.function, | |
| constructedClass: constructedClass, | |
| isConst: node.isConst, | |
| locationClass: tracking.locationClass, | |
| ); | |
| return node; | |
| } | |
| Expression _computeLocation( | |
| InvocationExpression node, | |
| FunctionNode function, | |
| Class? constructedClass, { | |
| bool isConst = false, | |
| required Class locationClass, | |
| }) { | |
| assert(constructedClass != null || !isConst); | |
| // For factory constructors we need to use the location specified as an | |
| // argument to the factory constructor rather than the location | |
| if (constructedClass != null && | |
| _currentFactory != null && | |
| _tracker._isSubclassWhere( | |
| constructedClass, | |
| (Class c) => c == _currentFactory!.enclosingClass!, | |
| ) && | |
| // If the constructor invocation is constant we cannot refer to the | |
| // location parameter of the surrounding factory since it isn't a | |
| // constant expression. | |
| !isConst) { | |
| final Variable? creationLocationParameter = _getNamedParameter( | |
| _currentFactory!.function, | |
| _creationLocationParameterName, | |
| ); | |
| if (creationLocationParameter != null) { | |
| return new VariableGet(creationLocationParameter); | |
| } | |
| } | |
| if (node.location == null) { | |
| return new NullLiteral(); | |
| } | |
| return _constructLocation( | |
| node.location!, | |
| locationClass: locationClass, | |
| name: | |
| constructedClass?.name ?? | |
| // For extension factory methods we use the name of the method. | |
| (function.parent! as Procedure).name.text, | |
| ); | |
| } | |
| void enterLibrary(Library library) { | |
| assert( | |
| _currentLibrary == null, | |
| "Attempting to enter library '${library.fileUri}' " | |
| "without having exited library '${_currentLibrary!.fileUri}'.", | |
| ); | |
| _currentLibrary = library; | |
| } | |
| void exitLibrary() { | |
| assert( | |
| _currentLibrary != null, | |
| "Attempting to exit a library without having entered one.", | |
| ); | |
| _currentLibrary = null; | |
| } | |
| } | |
| /// Rewrites all Widget constructors, constructor invocations, | |
| /// "Widget factories", and Widget factory invocations to add a | |
| /// parameter specifying the location the constructor/factory 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. | |
| /// | |
| /// A "Widget factory" is an extension instance method annotated with the | |
| /// `@widgetFactory` annotations. A _Location parameter is added to such methods | |
| /// and this is used as the location value for all Widget constructor | |
| /// invocations within the method. | |
| class WidgetCreatorTracker { | |
| bool _foundClasses = false; | |
| /// The [Widget] class defined in the `package:flutter` library. | |
| /// | |
| /// Used to perform is-tests to determine whether Dart constructor calls are | |
| /// creating [Widget] objects. | |
| Class? _widgetClass; | |
| /// The _Location class defined in the `package:flutter` library. | |
| Class? _locationClass; | |
| /// The _HasCreationLocation class defined in the `package:flutter` library. | |
| /// | |
| /// Marker interface indicating that a private _location field is available. | |
| Class? _hasCreationLocationClass; | |
| /// Annotation class used to mark an extension method as a "Widget factory". | |
| /// | |
| /// Widgets created within the body of an extension factory method will have | |
| /// their creation location set to the call site of the extension factory | |
| /// method. | |
| Class? _widgetFactoryClass; | |
| /// The _HasCreationLocation class defined in the `dart:developer` library. | |
| Class? _developerHasCreationLocationClass; | |
| /// The CreationLocation class defined in the `dart:developer` library. | |
| Class? _developerCreationLocationClass; | |
| void _resolveWellKnownClasses(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; | |
| // Legacy Case: Search for hardcoded Flutter classes. | |
| // TODO(http://dartbug.com/63225): Remove this once Flutter is migrated | |
| // to the new API. | |
| if (importUri.isScheme('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_; | |
| } else if (class_.name == '_WidgetFactory') { | |
| _widgetFactoryClass = class_; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // New Case: Search for classes in `dart:developer` | |
| if (importUri.isScheme('dart') && importUri.path == 'developer') { | |
| for (Class class_ in library.classes) { | |
| if (class_.name == '_HasCreationLocation') { | |
| _developerHasCreationLocationClass = class_; | |
| } else if (class_.name == 'CreationLocation') { | |
| _developerCreationLocationClass = class_; | |
| } | |
| } | |
| } | |
| } | |
| // TODO(johnniwinther): Require the [_widgetFactoryClass] once the | |
| // `widgetFactory` is stable in flutter. | |
| _foundClasses = | |
| (_widgetClass != null && | |
| _hasCreationLocationClass != null && | |
| _locationClass != null) || | |
| (_developerHasCreationLocationClass != null && | |
| _developerCreationLocationClass != null); | |
| } | |
| /// Modify [clazz] to add the location field 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 the tracking base class. | |
| void _transformClassImplementingTrackingClass( | |
| Class clazz, | |
| _TrackingClasses tracking, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| ) { | |
| if (clazz.fields.any( | |
| (Field field) => field.name.text == tracking.locationFieldName, | |
| )) { | |
| // This class has already been transformed. Skip | |
| return; | |
| } | |
| clazz.implementedTypes.add( | |
| new Supertype(tracking.hasCreationLocationClass, DartTypeList.empty), | |
| ); | |
| changedStructureNotifier?.registerClassHierarchyChange(clazz); | |
| // 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 the tracking class behave consistently. | |
| final Name fieldName = new Name( | |
| tracking.locationFieldName, | |
| tracking.hasCreationLocationClass.enclosingLibrary, | |
| ); | |
| final Field locationField = new Field.immutable( | |
| fieldName, | |
| type: new InterfaceType( | |
| tracking.locationClass, | |
| clazz.enclosingLibrary.nullable, | |
| ), | |
| isFinal: true, | |
| fieldReference: clazz.reference.canonicalName | |
| ?.getChildFromFieldWithName(fieldName) | |
| .reference, | |
| getterReference: clazz.reference.canonicalName | |
| ?.getChildFromFieldGetterWithName(fieldName) | |
| .reference, | |
| fileUri: clazz.fileUri, | |
| ); | |
| clazz.addField(locationField); | |
| final Set<Constructor> _handledConstructors = | |
| new Set<Constructor>.identity(); | |
| void handleConstructor(Constructor constructor) { | |
| if (!_handledConstructors.add(constructor)) { | |
| return; | |
| } | |
| assert( | |
| !_hasNamedParameter( | |
| constructor.function, | |
| _creationLocationParameterName, | |
| ), | |
| ); | |
| final NamedParameter parameter = new NamedParameter( | |
| parameterName: _creationLocationParameterName, | |
| type: new InterfaceType( | |
| tracking.locationClass, | |
| clazz.enclosingLibrary.nullable, | |
| ), | |
| defaultValue: new NullLiteral(), | |
| ); | |
| if (!_maybeAddNamedParameter(constructor.function, parameter)) { | |
| 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 below will fail due to the named | |
| // parameter not yet existing on the constructor. | |
| handleConstructor(initializer.target); | |
| } | |
| _maybeAddCreationLocationArgument( | |
| initializer.arguments, | |
| initializer.target.function, | |
| new VariableGet(parameter), | |
| ); | |
| hasRedirectingInitializer = true; | |
| break; | |
| } | |
| } | |
| if (!hasRedirectingInitializer) { | |
| constructor.initializers.add( | |
| new FieldInitializer(locationField, new VariableGet(parameter)), | |
| ); | |
| // 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), tracking.locationClass.thisType), | |
| // conditionStartOffset: constructor.fileOffset, | |
| // conditionEndOffset: constructor.fileOffset, | |
| // ))); | |
| } | |
| } | |
| // Add named parameters to all constructors. | |
| clazz.constructors.forEach(handleConstructor); | |
| } | |
| /// Transform the given [libraries]. | |
| /// | |
| /// The [moduleLibraries] are searched for the Widget class, | |
| /// the _Location class, the _HasCreationLocation class and the | |
| /// _WidgetFactory class. | |
| /// If the component does not contain them, the ones from a previous run is | |
| /// used (if any), otherwise no transformation is performed. | |
| /// | |
| /// Upon transformation the [changedStructureNotifier] (if provided) is used | |
| /// to notify the listener that that class hierarchy of certain classes has | |
| /// changed. This is necessary for instance when doing an incremental | |
| /// compilation where the class hierarchy is kept between compiles and thus | |
| /// has to be kept up to date. | |
| void transform( | |
| List<Library> libraries, | |
| List<Library> moduleLibraries, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| ) { | |
| if (libraries.isEmpty) { | |
| return; | |
| } | |
| _resolveWellKnownClasses(moduleLibraries); | |
| if (!_foundClasses) { | |
| // Neither package:flutter nor dart:developer tracking classes found. | |
| return; | |
| } | |
| final Set<Class> transformedClasses = new Set<Class>.identity(); | |
| final Set<Extension> transformedExtensions = new Set<Extension>.identity(); | |
| final Set<Library> librariesToTransform = new Set<Library>.identity() | |
| ..addAll(libraries); | |
| for (Library library in libraries) { | |
| for (Class class_ in library.classes) { | |
| _transformWidgetConstructors( | |
| librariesToTransform, | |
| transformedClasses, | |
| class_, | |
| changedStructureNotifier, | |
| ); | |
| } | |
| for (Extension extension in library.extensions) { | |
| _transformExtension( | |
| librariesToTransform, | |
| transformedExtensions, | |
| extension, | |
| ); | |
| } | |
| } | |
| // Transform call sites to pass the location parameter. | |
| final _WidgetCallSiteTransformer callsiteTransformer = | |
| new _WidgetCallSiteTransformer(tracker: this); | |
| for (Library library in libraries) { | |
| callsiteTransformer.enterLibrary(library); | |
| library.transformChildren(callsiteTransformer); | |
| callsiteTransformer.exitLibrary(); | |
| } | |
| } | |
| _TrackingClasses? _getTrackingClasses(Class clazz) { | |
| // New Case: Check for 'pragma('track-creation-locations')' annotation. | |
| if (_hasTrackCreationLocationsPragmaAnnotation(clazz)) { | |
| if (_developerHasCreationLocationClass != null && | |
| _developerCreationLocationClass != null) { | |
| return new _TrackingClasses( | |
| hasCreationLocationClass: _developerHasCreationLocationClass!, | |
| locationClass: _developerCreationLocationClass!, | |
| locationFieldName: _locationFieldName, | |
| ); | |
| } | |
| } | |
| // Legacy Case: Check for widget class. | |
| if (_isSubclassOfWidget(clazz)) { | |
| if (_hasCreationLocationClass != null && _locationClass != null) { | |
| return new _TrackingClasses( | |
| hasCreationLocationClass: _hasCreationLocationClass!, | |
| locationClass: _locationClass!, | |
| locationFieldName: _locationFieldName, | |
| ); | |
| } | |
| } | |
| return null; | |
| } | |
| bool _isSubclassOfWidget(Class clazz) { | |
| if (_widgetClass == null) return false; | |
| return _isSubclassWhere(clazz, (Class c) => c == _widgetClass); | |
| } | |
| bool _hasTrackCreationLocationsPragmaAnnotation(Class clazz) { | |
| if (_developerHasCreationLocationClass == null) return false; | |
| return _isSubclassWhere( | |
| clazz, | |
| (Class c) => _hasPragmaAnnotation(c, 'track-creation-locations'), | |
| ); | |
| } | |
| bool _hasPragmaAnnotation(Annotatable node, String pragmaName) { | |
| for (Expression annotation in node.annotations) { | |
| if (_getPragmaName(annotation) == pragmaName) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| String? _getPragmaName(Expression expression) { | |
| // Direct invocation, e.g. `@pragma('...')`. | |
| // Applies before constant evaluation (newly compiled modules). | |
| if (expression | |
| case RedirectingFactoryInvocation( | |
| expression: final ConstructorInvocation invocation, | |
| ) || | |
| final ConstructorInvocation invocation) { | |
| final Class enclosingClass = invocation.target.enclosingClass; | |
| if (enclosingClass.name == 'pragma' && | |
| enclosingClass.enclosingLibrary.importUri.toString() == 'dart:core') { | |
| if (invocation.arguments.positional.isNotEmpty) { | |
| final Expression firstArg = invocation.arguments.positional.first; | |
| if (firstArg is StringLiteral) { | |
| return firstArg.value; | |
| } | |
| } | |
| } | |
| } | |
| // Aliased constant, e.g. `@widgetFactory` | |
| // with `const widgetFactory = pragma('...');`. | |
| // Applies before constant evaluation (newly compiled modules). | |
| if (expression is StaticGet) { | |
| final Member target = expression.target; | |
| if (target is Field && target.isConst) { | |
| if (target.initializer != null) { | |
| return _getPragmaName(target.initializer!); | |
| } | |
| } | |
| } | |
| // Evaluated constant pragma. | |
| // Applies after constant evaluation (incremental or modular compilation). | |
| if (expression is ConstantExpression) { | |
| final Constant constant = expression.constant; | |
| if (constant is InstanceConstant) { | |
| final Class enclosingClass = constant.classNode; | |
| if (enclosingClass.name == 'pragma' && | |
| enclosingClass.enclosingLibrary.importUri.toString() == | |
| 'dart:core') { | |
| for (final Constant value in constant.fieldValues.values) { | |
| if (value is StringConstant) { | |
| return value.value; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| Class? _getFactoryLocationClass(Procedure method) { | |
| if (_hasPragmaAnnotation(method, 'track-creation-locations')) { | |
| return _developerCreationLocationClass; | |
| } | |
| if (_hasWidgetFactoryAnnotation(method)) { | |
| return _locationClass; | |
| } | |
| return null; | |
| } | |
| bool _isSubclassWhere(Class a, bool Function(Class b) predicate) { | |
| // TODO(askesc): Cache results. | |
| // TODO(askesc): Test for subtype rather than subclass. | |
| Class? current = a; | |
| while (current != null) { | |
| if (predicate(current)) return true; | |
| current = current.superclass; | |
| } | |
| return false; | |
| } | |
| bool _hasWidgetFactoryAnnotation(Procedure node) => | |
| _isAnnotatedWithNamedValueOfType( | |
| node, | |
| _widgetFactoryClass, | |
| _widgetFactoryClass, | |
| ); | |
| bool _isAnnotatedWithNamedValueOfType( | |
| Annotatable node, | |
| Class? annotationClass, | |
| Class? typeClass, | |
| ) { | |
| if (annotationClass == null || typeClass == null) return false; | |
| return node.annotations.any((annotation) { | |
| if (annotation is! StaticGet) { | |
| return false; | |
| } | |
| final Member target = annotation.target; | |
| if (target is! Field) { | |
| return false; | |
| } | |
| final DartType type = target.type; | |
| if (type is! InterfaceType) { | |
| return false; | |
| } | |
| if (type.nullability == Nullability.nullable) { | |
| return false; | |
| } | |
| return type.classNode == typeClass; | |
| }); | |
| } | |
| void _transformWidgetConstructors( | |
| Set<Library> librariesToBeTransformed, | |
| Set<Class> transformedClasses, | |
| Class clazz, | |
| ChangedStructureNotifier? changedStructureNotifier, | |
| ) { | |
| final _TrackingClasses? tracking = _getTrackingClasses(clazz); | |
| if (tracking == null || | |
| !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!, | |
| changedStructureNotifier, | |
| ); | |
| } | |
| for (Procedure procedure in clazz.procedures) { | |
| if (procedure.isFactory) { | |
| _maybeAddNamedParameter( | |
| procedure.function, | |
| new NamedParameter( | |
| parameterName: _creationLocationParameterName, | |
| type: new InterfaceType( | |
| tracking.locationClass, | |
| clazz.enclosingLibrary.nullable, | |
| ), | |
| defaultValue: new NullLiteral(), | |
| ), | |
| ); | |
| } | |
| } | |
| // Handle the widget class and classes that implement but do not extend the | |
| // widget class. | |
| if (clazz.superclass == null || | |
| _getTrackingClasses(clazz.superclass!)?.hasCreationLocationClass != | |
| tracking.hasCreationLocationClass) { | |
| _transformClassImplementingTrackingClass( | |
| clazz, | |
| tracking, | |
| changedStructureNotifier, | |
| ); | |
| return; | |
| } | |
| final Set<Constructor> _handledConstructors = | |
| new Set<Constructor>.identity(); | |
| void handleConstructor(Constructor constructor) { | |
| if (!_handledConstructors.add(constructor)) { | |
| return; | |
| } | |
| final NamedParameter parameter = new NamedParameter( | |
| parameterName: _creationLocationParameterName, | |
| type: new InterfaceType( | |
| tracking.locationClass, | |
| clazz.enclosingLibrary.nullable, | |
| ), | |
| defaultValue: new NullLiteral(), | |
| ); | |
| if (_hasNamedParameter( | |
| constructor.function, | |
| _creationLocationParameterName, | |
| )) { | |
| // Constructor was already rewritten. | |
| // TODO(jacobr): is this case actually hit? | |
| return; | |
| } | |
| if (!_maybeAddNamedParameter(constructor.function, parameter)) { | |
| 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(parameter), | |
| ); | |
| } else if (initializer is SuperInitializer) { | |
| final Class superclass = initializer.target.enclosingClass; | |
| if (_getTrackingClasses(superclass)?.hasCreationLocationClass == | |
| tracking.hasCreationLocationClass) { | |
| _maybeAddCreationLocationArgument( | |
| initializer.arguments, | |
| initializer.target.function, | |
| new VariableGet(parameter), | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| clazz.constructors.forEach(handleConstructor); | |
| } | |
| void _transformExtension( | |
| Set<Library> librariesToBeTransformed, | |
| Set<Extension> transformedExtensions, | |
| Extension extension, | |
| ) { | |
| // TODO(johnniwinther): We should have a lint for unsupported use of | |
| // `@widgetFactory`. | |
| if (!librariesToBeTransformed.contains(extension.enclosingLibrary) || | |
| !transformedExtensions.add(extension)) { | |
| return; | |
| } | |
| for (ExtensionMemberDescriptor member in extension.memberDescriptors) { | |
| if (member.isStatic) { | |
| // We could support static extension methods but it is not clear that | |
| // there is a use case for this. | |
| continue; | |
| } | |
| final Procedure? method = member.memberReference?.asProcedure; | |
| if (method != null) { | |
| _transformExtensionProcedure(method, extension); | |
| } | |
| final Procedure? tearOff = member.tearOffReference?.asProcedure; | |
| if (tearOff != null) { | |
| _transformExtensionProcedure(tearOff, extension); | |
| } | |
| } | |
| } | |
| void _transformExtensionProcedure(Procedure method, Extension extension) { | |
| final Class? locationClass = _getFactoryLocationClass(method); | |
| if (locationClass == null) { | |
| return; | |
| } | |
| _maybeAddNamedParameter( | |
| method.function, | |
| new NamedParameter( | |
| parameterName: _creationLocationParameterName, | |
| type: new InterfaceType( | |
| locationClass, | |
| extension.enclosingLibrary.nullable, | |
| ), | |
| defaultValue: new NullLiteral(), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Holds the set of creation location classes, either | |
| /// from `package:flutter` or `dart:developer`. | |
| class _TrackingClasses { | |
| final Class hasCreationLocationClass; | |
| final Class locationClass; | |
| final String locationFieldName; | |
| new({ | |
| required this.hasCreationLocationClass, | |
| required this.locationClass, | |
| required this.locationFieldName, | |
| }); | |
| } |