|
| 1 | +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:analysis_server/src/utilities/extensions/numeric.dart'; |
| 6 | +import 'package:analyzer/dart/analysis/results.dart'; |
| 7 | +import 'package:analyzer/dart/element/element2.dart'; |
| 8 | +import 'package:analyzer/src/dart/ast/ast.dart'; |
| 9 | +import 'package:analyzer/src/dart/element/element.dart'; |
| 10 | +import 'package:analyzer/src/utilities/extensions/ast.dart'; |
| 11 | +import 'package:analyzer/src/utilities/extensions/flutter.dart'; |
| 12 | + |
| 13 | +/// Information about the arguments and parameters for an invocation. |
| 14 | +typedef EditableInvocationInfo = |
| 15 | + ({ |
| 16 | + List<FormalParameterElement> parameters, |
| 17 | + Map<FormalParameterElement, Expression> parameterArguments, |
| 18 | + Map<FormalParameterElement, int> positionalParameterIndexes, |
| 19 | + ArgumentList argumentList, |
| 20 | + int numPositionals, |
| 21 | + int numSuppliedPositionals, |
| 22 | + }); |
| 23 | + |
| 24 | +/// A mixin that provides functionality for locating arguments and associated |
| 25 | +/// parameters in a document to allow a client to provide editing capabilities. |
| 26 | +mixin EditableArgumentsMixin { |
| 27 | + /// Gets information about an invocation at [offset] in [result] that can be |
| 28 | + /// edited. |
| 29 | + EditableInvocationInfo? getInvocationInfo( |
| 30 | + ResolvedUnitResult result, |
| 31 | + int offset, |
| 32 | + ) { |
| 33 | + var node = result.unit.nodeCovering(offset: offset); |
| 34 | + // Walk up to find an invocation that is widget creation. |
| 35 | + var invocation = node?.thisOrAncestorMatching((node) { |
| 36 | + return switch (node) { |
| 37 | + InstanceCreationExpression() => node.isWidgetCreation, |
| 38 | + InvocationExpressionImpl() => node.isWidgetFactory, |
| 39 | + _ => false, |
| 40 | + }; |
| 41 | + }); |
| 42 | + |
| 43 | + // Return the related argument list. |
| 44 | + var (parameters, argumentList) = switch (invocation) { |
| 45 | + InstanceCreationExpression() => ( |
| 46 | + invocation.constructorName.element?.formalParameters, |
| 47 | + invocation.argumentList, |
| 48 | + ), |
| 49 | + MethodInvocation( |
| 50 | + methodName: Identifier(element: ExecutableElement2 element), |
| 51 | + ) => |
| 52 | + (element.formalParameters, invocation.argumentList), |
| 53 | + _ => (null, null), |
| 54 | + }; |
| 55 | + |
| 56 | + if (parameters == null || argumentList == null) { |
| 57 | + return null; |
| 58 | + } |
| 59 | + |
| 60 | + var numPositionals = parameters.where((p) => p.isPositional).length; |
| 61 | + var numSuppliedPositionals = |
| 62 | + argumentList.arguments |
| 63 | + .where((argument) => argument is! NamedExpression) |
| 64 | + .length; |
| 65 | + |
| 66 | + // Build a map of parameters to their positional index so we can tell |
| 67 | + // whether a parameter that doesn't already have an argument will be |
| 68 | + // editable (positionals can only be added if all previous positionals |
| 69 | + // exist). |
| 70 | + var currentParameterIndex = 0; |
| 71 | + var positionalParameterIndexes = { |
| 72 | + for (var parameter in parameters) |
| 73 | + if (parameter.isPositional) parameter: currentParameterIndex++, |
| 74 | + }; |
| 75 | + |
| 76 | + // Build a map of the parameters that have arguments so we can put them |
| 77 | + // first or look up whether a parameter is editable based on the argument. |
| 78 | + var parameterArguments = { |
| 79 | + for (var argument in argumentList.arguments) |
| 80 | + if (argument.correspondingParameter case var parameter?) |
| 81 | + parameter: argument, |
| 82 | + }; |
| 83 | + |
| 84 | + return ( |
| 85 | + parameters: parameters, |
| 86 | + positionalParameterIndexes: positionalParameterIndexes, |
| 87 | + parameterArguments: parameterArguments, |
| 88 | + argumentList: argumentList, |
| 89 | + numPositionals: numPositionals, |
| 90 | + numSuppliedPositionals: numSuppliedPositionals, |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + /// Checks whether [argument] is editable and if not, returns a human-readable |
| 95 | + /// description why. |
| 96 | + String? getNotEditableReason({ |
| 97 | + required Expression? argument, |
| 98 | + required int? positionalIndex, |
| 99 | + required int numPositionals, |
| 100 | + required int numSuppliedPositionals, |
| 101 | + }) { |
| 102 | + // If the argument has an existing value, editability is based only on that |
| 103 | + // value. |
| 104 | + if (argument != null) { |
| 105 | + return switch (argument) { |
| 106 | + AdjacentStrings() => "Adjacent strings can't be edited", |
| 107 | + StringInterpolation() => "Interpolated strings can't be edited", |
| 108 | + SimpleStringLiteral() when argument.value.contains('\n') => |
| 109 | + "Strings containing newlines can't be edited", |
| 110 | + _ => null, |
| 111 | + }; |
| 112 | + } |
| 113 | + |
| 114 | + // If we are missing positionals, we can only add this one if it is the next |
| 115 | + // (first missing) one. |
| 116 | + if (positionalIndex != null && numSuppliedPositionals < numPositionals) { |
| 117 | + // To be allowed, we must be the next one. Eg. our index is equal to the |
| 118 | + // length/count of the existing ones. |
| 119 | + if (positionalIndex != numSuppliedPositionals) { |
| 120 | + return 'A value for the ${(positionalIndex + 1).toStringWithSuffix()} ' |
| 121 | + "parameter can't be added until a value for all preceding " |
| 122 | + 'positional parameters have been added.'; |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + return null; |
| 127 | + } |
| 128 | + |
| 129 | + /// Returns the name of an enum constant prefixed with the enum name. |
| 130 | + String? getQualifiedEnumConstantName(FieldElement2 enumConstant) { |
| 131 | + var enumName = enumConstant.enclosingElement2.name3; |
| 132 | + var name = enumConstant.name3; |
| 133 | + return enumName != null && name != null ? '$enumName.$name' : null; |
| 134 | + } |
| 135 | + |
| 136 | + /// Returns a list of the constants of an enum constant prefixed with the enum |
| 137 | + /// name. |
| 138 | + List<String> getQualifiedEnumConstantNames(EnumElement2 element3) => |
| 139 | + element3.constants2.map(getQualifiedEnumConstantName).nonNulls.toList(); |
| 140 | +} |
| 141 | + |
| 142 | +extension on InvocationExpressionImpl { |
| 143 | + /// Whether this is an invocation for an extension method that has the |
| 144 | + /// `@widgetFactory` annotation. |
| 145 | + bool get isWidgetFactory { |
| 146 | + // Only consider functions that return widgets. |
| 147 | + if (!staticType.isWidgetType) { |
| 148 | + return false; |
| 149 | + } |
| 150 | + |
| 151 | + // We only support @widgetFactory on extension methods. |
| 152 | + var element = switch (function) { |
| 153 | + Identifier(:var element) |
| 154 | + when element?.enclosingElement2 is ExtensionElement2 => |
| 155 | + element, |
| 156 | + _ => null, |
| 157 | + }; |
| 158 | + |
| 159 | + return switch (element) { |
| 160 | + FragmentedAnnotatableElementMixin(:var metadata2) => |
| 161 | + metadata2.hasWidgetFactory, |
| 162 | + _ => false, |
| 163 | + }; |
| 164 | + } |
| 165 | +} |
0 commit comments