| // Copyright (c) 2022, 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:analysis_server/src/services/correction/assist.dart'; |
| import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart'; |
| import 'package:analysis_server/src/utilities/extensions/range_factory.dart'; |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/source/source_range.dart'; |
| import 'package:analyzer_plugin/utilities/assist/assist.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| class ConvertToSuperParameters extends CorrectionProducer { |
| @override |
| AssistKind get assistKind => DartAssistKind.CONVERT_TO_SUPER_PARAMETERS; |
| |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| if (!libraryElement.featureSet.isEnabled(Feature.super_parameters)) { |
| // If the library doesn't support super_parameters then the change isn't |
| // appropriate. |
| return; |
| } |
| var constructor = _findConstructor(); |
| if (constructor == null) { |
| // If this isn't a constructor declaration then the change isn't |
| // appropriate. |
| return; |
| } |
| var superInvocation = _superInvocation(constructor); |
| if (superInvocation == null) { |
| // If there isn't an explicit invocation of a super constructor then the |
| // change isn't appropriate. Note that this also rules out factory |
| // constructors because factory constructors can't have initializers. |
| return; |
| } |
| var superConstructor = superInvocation.staticElement; |
| if (superConstructor == null) { |
| // If the super constructor wasn't resolved then we can't apply the |
| // change. |
| return; |
| } |
| // Find the arguments that can be converted. Named arguments are added to |
| // [named]. Positional arguments are added to [positional], but the list is |
| // set to `null` if a positional argument is found that can't be converted |
| // because either all of the positional parameters must be converted or none |
| // of them can be converted. |
| var parameterMap = _parameterMap(constructor.parameters); |
| List<_ParameterData>? positional = []; |
| var named = <_ParameterData>[]; |
| var arguments = superInvocation.argumentList.arguments; |
| for (var argumentIndex = 0; |
| argumentIndex < arguments.length; |
| argumentIndex++) { |
| var argument = arguments[argumentIndex]; |
| if (argument is NamedExpression) { |
| var parameterAndElement = |
| _parameterFor(parameterMap, argument.expression); |
| if (parameterAndElement != null && parameterAndElement.isNamed) { |
| var data = _dataForParameter( |
| parameterAndElement.parameter, |
| parameterAndElement.element, |
| argumentIndex, |
| argument.staticParameterElement, |
| ); |
| if (data != null) { |
| named.add(data); |
| } |
| } |
| } else if (positional != null) { |
| var parameterAndElement = _parameterFor(parameterMap, argument); |
| if (parameterAndElement == null || !parameterAndElement.isPositional) { |
| positional = null; |
| } else { |
| var data = _dataForParameter( |
| parameterAndElement.parameter, |
| parameterAndElement.element, |
| argumentIndex, |
| argument.staticParameterElement, |
| ); |
| if (data == null) { |
| positional = null; |
| } else { |
| positional.add(data); |
| } |
| } |
| } |
| } |
| if (positional != null && !_inOrder(positional)) { |
| positional = null; |
| } |
| // At this point: |
| // 1. `positional` will be `null` if |
| // - there is at least one positional argument that can't be converted, |
| // which implies that there are no positional arguments that can be |
| // converted, or |
| // - if the order of the positional parameters doesn't match the order of |
| // the positional arguments. |
| // 2. `positional` will be empty if there are no positional arguments at |
| // all. |
| // 3. `named` will be empty if there are no named arguments that can be |
| // converted. |
| if ((positional == null || positional.isEmpty) && named.isEmpty) { |
| // There are no parameters that can be converted. |
| return; |
| } |
| |
| var allParameters = <_ParameterData>[...?positional, ...named]; |
| |
| var argumentsToDelete = |
| allParameters.map((data) => data.argumentIndex).toList(); |
| argumentsToDelete.sort(); |
| |
| await builder.addDartFileEdit(file, (builder) { |
| // Convert the parameters. |
| for (var parameterData in allParameters) { |
| var typeToDelete = parameterData.typeToDelete; |
| if (typeToDelete == null) { |
| builder.addSimpleInsertion(parameterData.nameOffset, 'super.'); |
| } else { |
| var primaryRange = typeToDelete.primaryRange; |
| if (primaryRange == null) { |
| builder.addSimpleInsertion(parameterData.nameOffset, 'super.'); |
| } else { |
| builder.addSimpleReplacement(primaryRange, 'super.'); |
| } |
| var parameterRange = typeToDelete.parameterRange; |
| if (parameterRange != null) { |
| builder.addDeletion(parameterRange); |
| } |
| } |
| var defaultValueRange = parameterData.defaultValueRange; |
| if (defaultValueRange != null) { |
| builder.addDeletion(defaultValueRange); |
| } |
| } |
| |
| // Remove the corresponding arguments. |
| if (argumentsToDelete.length == arguments.length && |
| superInvocation.constructorName == null) { |
| var initializers = constructor.initializers; |
| SourceRange initializerRange; |
| if (initializers.length == 1) { |
| initializerRange = |
| range.endEnd(constructor.parameters, superInvocation); |
| } else { |
| initializerRange = range.nodeInList(initializers, superInvocation); |
| } |
| builder.addDeletion(initializerRange); |
| } else { |
| var ranges = range.nodesInList(arguments, argumentsToDelete); |
| for (var range in ranges) { |
| builder.addDeletion(range); |
| } |
| } |
| }); |
| } |
| |
| /// If the [parameter] can be converted into a super initializing formal |
| /// parameter then return the data needed to do so. |
| _ParameterData? _dataForParameter( |
| _Parameter parameter, |
| ParameterElement thisParameter, |
| int argumentIndex, |
| ParameterElement? superParameter) { |
| if (superParameter == null) { |
| return null; |
| } |
| // If the type of the `thisParameter` isn't a subtype of the type of the |
| // super parameter, then the change isn't appropriate. |
| var superType = superParameter.type; |
| var thisType = thisParameter.type; |
| if (!typeSystem.isSubtypeOf(thisType, superType)) { |
| return null; |
| } |
| var identifier = parameter.parameter.identifier; |
| if (identifier == null) { |
| // This condition should never occur, but the test is here to promote the |
| // type. |
| return null; |
| } |
| // Return the data. |
| return _ParameterData( |
| argumentIndex: argumentIndex, |
| defaultValueRange: _defaultValueRange( |
| parameter.parameter, superParameter, thisParameter), |
| nameOffset: identifier.offset, |
| parameterIndex: parameter.index, |
| typeToDelete: superType == thisType ? _type(parameter.parameter) : null, |
| ); |
| } |
| |
| /// Return the range of the default value associated with the [parameter], or |
| /// `null` if the parameter doesn't have a default value or if the default |
| /// value is not the same as the default value in the super constructor. |
| SourceRange? _defaultValueRange(FormalParameter parameter, |
| ParameterElement superParameter, ParameterElement thisParameter) { |
| if (parameter is DefaultFormalParameter) { |
| var defaultValue = parameter.defaultValue; |
| if (defaultValue != null) { |
| var superDefault = superParameter.computeConstantValue(); |
| var thisDefault = thisParameter.computeConstantValue(); |
| if (superDefault != null && superDefault == thisDefault) { |
| return range.endEnd(parameter.identifier!, defaultValue); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /// Return the constructor to be converted, or `null` if the cursor is not on |
| /// the name of a constructor. |
| ConstructorDeclaration? _findConstructor() { |
| final node = this.node; |
| if (node is SimpleIdentifier) { |
| var parent = node.parent; |
| if (parent is ConstructorDeclaration) { |
| return parent; |
| } else if (parent is ConstructorName) { |
| var grandparent = parent.parent; |
| if (grandparent is ConstructorDeclaration) { |
| return grandparent; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /// Return `true` if the given list of [parameterData] is in order by the |
| /// index of the parameters. The list is known to be in order by the argument |
| /// positions, so this test is used to ensure that the order won't be changed |
| /// if the parameters are converted. |
| bool _inOrder(List<_ParameterData> parameterData) { |
| var previousIndex = -1; |
| for (var data in parameterData) { |
| var index = data.parameterIndex; |
| if (index < previousIndex) { |
| return false; |
| } |
| previousIndex = index; |
| } |
| return true; |
| } |
| |
| /// Return the parameter element corresponding to the [expression], or `null` |
| /// if the expression isn't a simple reference to one of the parameters in the |
| /// constructor being converted. |
| _ParameterAndElement? _parameterFor( |
| Map<ParameterElement, _Parameter> parameterMap, Expression expression) { |
| if (expression is SimpleIdentifier) { |
| var element = expression.staticElement; |
| var parameter = parameterMap[element]; |
| if (element is ParameterElement && parameter != null) { |
| return _ParameterAndElement(element, parameter); |
| } |
| } |
| return null; |
| } |
| |
| /// Return a map from parameter elements to the parameters that define those |
| /// elements. |
| Map<ParameterElement, _Parameter> _parameterMap( |
| FormalParameterList parameterList) { |
| var map = <ParameterElement, _Parameter>{}; |
| var parameters = parameterList.parameters; |
| for (var i = 0; i < parameters.length; i++) { |
| var parameter = parameters[i]; |
| var element = parameter.declaredElement; |
| if (element != null) { |
| map[element] = _Parameter(parameter, i); |
| } |
| } |
| return map; |
| } |
| |
| /// Return the invocation of the super constructor. |
| SuperConstructorInvocation? _superInvocation( |
| ConstructorDeclaration constructor) { |
| var initializers = constructor.initializers; |
| // Search all of the initializers in case the code is invalid, but start |
| // from the end because the code will usually be correct. |
| for (var i = initializers.length - 1; i >= 0; i--) { |
| var initializer = initializers[i]; |
| if (initializer is SuperConstructorInvocation) { |
| return initializer; |
| } |
| } |
| return null; |
| } |
| |
| /// Return data about the type annotation on the [parameter]. This is the |
| /// information about the ranges of text that need to be removed in order to |
| /// remove the type annotation. |
| _TypeData? _type(FormalParameter parameter) { |
| if (parameter is DefaultFormalParameter) { |
| return _type(parameter.parameter); |
| } else if (parameter is SimpleFormalParameter) { |
| var typeAnnotation = parameter.type; |
| if (typeAnnotation != null) { |
| return _TypeData( |
| primaryRange: |
| range.startStart(typeAnnotation, parameter.identifier!)); |
| } |
| } else if (parameter is FunctionTypedFormalParameter) { |
| var returnType = parameter.returnType; |
| return _TypeData( |
| primaryRange: returnType != null |
| ? range.startStart(returnType, parameter.identifier) |
| : null, |
| parameterRange: range.node(parameter.parameters)); |
| } |
| return null; |
| } |
| |
| /// Return an instance of this class. Used as a tear-off in `AssistProcessor`. |
| static ConvertToSuperParameters newInstance() => ConvertToSuperParameters(); |
| } |
| |
| /// Information about a single parameter. |
| class _Parameter { |
| final FormalParameter parameter; |
| |
| final int index; |
| |
| _Parameter(this.parameter, this.index); |
| } |
| |
| /// A data class used to avoid a null check. |
| class _ParameterAndElement { |
| final ParameterElement element; |
| |
| final _Parameter parameter; |
| |
| _ParameterAndElement(this.element, this.parameter); |
| |
| bool get isNamed => element.isNamed; |
| |
| bool get isPositional => element.isPositional; |
| } |
| |
| /// Information used to convert a single parameter. |
| class _ParameterData { |
| /// The type annotation that should be deleted from the parameter list, or |
| /// `null` if there is no type annotation to delete or if the type should not |
| /// be deleted. |
| final _TypeData? typeToDelete; |
| |
| /// The offset of the name. |
| final int nameOffset; |
| |
| /// The range of the default value that is to be deleted from the parameter |
| /// list, or `null` if there is no default value, the default value isn't to |
| /// be deleted. |
| final SourceRange? defaultValueRange; |
| |
| /// The index of the parameter to be updated. |
| final int parameterIndex; |
| |
| /// The index of the argument to be deleted. |
| final int argumentIndex; |
| |
| /// Initialize a newly create data object. |
| _ParameterData( |
| {required this.typeToDelete, |
| required this.nameOffset, |
| required this.defaultValueRange, |
| required this.parameterIndex, |
| required this.argumentIndex}); |
| } |
| |
| /// Information about the ranges of text that need to be removed in order to |
| /// remove a type annotation. |
| class _TypeData { |
| SourceRange? primaryRange; |
| |
| SourceRange? parameterRange; |
| |
| _TypeData({required this.primaryRange, this.parameterRange}); |
| } |