Skip to content

Commit ed49d04

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Extract some shared code from editableArguments to be used by editArgument
This doesn't change any functionality, it just extracts some code from the editableArguments handler into a mixin because the editArgument handler will want to reuse some of this logic to locate the argument/parameter and ensure it is allowed to be edited. Change-Id: Ibe9f1350c977b470847cebe2ecf7a7bec5256000 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/398440 Reviewed-by: Elliott Brooks <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent c54255f commit ed49d04

File tree

4 files changed

+195
-143
lines changed

4 files changed

+195
-143
lines changed

pkg/analysis_server/lib/src/lsp/constants.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ abstract final class CustomMethods {
153153
static const dartTextDocumentContentDidChange = Method(
154154
'dart/textDocumentContentDidChange',
155155
);
156+
157+
/// Method for requesting the set of editable arguments at a location in a
158+
/// document.
156159
static const dartTextDocumentEditableArguments = Method(
157160
'experimental/dart/textDocument/editableArguments',
158161
);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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

Comments
 (0)