Skip to content

Commit aeb3a58

Browse files
DanTupCommit Queue
authored andcommitted
Introduce variables for non-simple parameters when inlining methods
When inlining a method call, if a parameter is using multiple times then (except in the case of variables) we should capture the argument value in a variable to avoid potential side-effects (or worsened performance) of evaluating the expression multiple times. Fixes #52844 Change-Id: Icc52e478fd27cedfb68a032af19cc58bb1444167 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452780 Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent 4d76a3c commit aeb3a58

File tree

2 files changed

+478
-22
lines changed

2 files changed

+478
-22
lines changed

pkg/analysis_server/lib/src/services/refactoring/legacy/inline_method.dart

Lines changed: 188 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:analysis_server/src/protocol_server.dart' hide Element;
6+
import 'package:analysis_server/src/services/correction/dart/convert_null_check_to_null_aware_element_or_entry.dart';
67
import 'package:analysis_server/src/services/correction/status.dart';
78
import 'package:analysis_server/src/services/correction/util.dart';
89
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
@@ -47,7 +48,7 @@ SourceRange _getLocalsConflictingRange(AstNode node) {
4748

4849
/// Returns the source which should replace given invocation with given
4950
/// arguments.
50-
Future<String> _getMethodSourceForInvocation(
51+
Future<_InlineMethodResult> _getMethodSourceForInvocation(
5152
RefactoringStatus status,
5253
ChangeBuilder builder,
5354
String file,
@@ -58,9 +59,14 @@ Future<String> _getMethodSourceForInvocation(
5859
AstNode contextNode,
5960
Expression? targetExpression,
6061
List<Expression> arguments,
62+
Set<String> previouslyIntroducedVariableNames,
63+
Map<FormalParameterElement, String> parameterToVariableName,
6164
) async {
6265
// prepare edits to replace parameters with arguments
66+
var conflictingNames = _getNamesConflictingAt(contextNode);
67+
conflictingNames.addAll(previouslyIntroducedVariableNames);
6368
var edits = <SourceEdit>[];
69+
var variableDeclarations = <_VariableDeclaration>[];
6470
await builder.addDartFileEdit(file, (builder) {
6571
part._typeParameters.forEach((
6672
TypeParameterElement element,
@@ -126,6 +132,42 @@ Future<String> _getMethodSourceForInvocation(
126132
argumentSource = parameter.defaultValueCode;
127133
argumentSource ??= 'null';
128134
}
135+
136+
var existingVariableName = parameterToVariableName[parameter];
137+
var addVariable =
138+
existingVariableName != null ||
139+
_shouldIntroduceVariable(argument, occurrences.length);
140+
if (addVariable) {
141+
// Only add a new variable if we haven't already produced one in a
142+
// previous call to this method (we may be called twice, once for
143+
// statements and once for the return expression).
144+
if (existingVariableName == null) {
145+
var originalName = parameter.displayName;
146+
if (originalName.isEmpty) {
147+
originalName = 'p';
148+
}
149+
var uniqueName = existingVariableName = _getUniqueName(
150+
originalName,
151+
conflictingNames,
152+
);
153+
conflictingNames.add(uniqueName);
154+
parameterToVariableName[parameter] = uniqueName;
155+
156+
// Record the variable declaration to be inserted above the inline code.
157+
// We can't insert it now because this might be an expression and not a
158+
// block of statements (for example `var a = inlineMe();`).
159+
variableDeclarations.add(
160+
_VariableDeclaration(uniqueName, argumentSource),
161+
);
162+
}
163+
164+
// Update the values used for replacing the expression in the body
165+
// to this newly-created variable.
166+
argumentPrecedence = Precedence.primary;
167+
argumentSource = existingVariableName;
168+
argument = null;
169+
}
170+
129171
// replace all occurrences of this parameter
130172
for (var occurrence in occurrences) {
131173
AstNode nodeToReplace = occurrence.identifier;
@@ -134,7 +176,7 @@ Future<String> _getMethodSourceForInvocation(
134176
if (occurrence.identifier.parent
135177
case InterpolationExpression interpolation) {
136178
switch (argument) {
137-
case SimpleIdentifier():
179+
case SimpleIdentifier() || null:
138180
occurrenceArgumentSource = argumentSource;
139181
case SingleStringLiteral(canDiscardSingleQuotes: true):
140182
nodeToReplace = interpolation;
@@ -150,6 +192,18 @@ Future<String> _getMethodSourceForInvocation(
150192
} else {
151193
occurrenceArgumentSource = argumentSource;
152194
}
195+
196+
// If we're replacing this occurrence with a variable and it's
197+
// parenthesized (and the parenthesized expression is only this node)
198+
// then we can remove the parens.
199+
if (addVariable) {
200+
var parent = occurrence.identifier.parent;
201+
if (parent is ParenthesizedExpression &&
202+
parent.expression == occurrence.identifier) {
203+
nodeToReplace = parent;
204+
}
205+
}
206+
153207
// do replace
154208
var nodeToReplaceRange = range.offsetBy(
155209
range.node(nodeToReplace),
@@ -181,19 +235,10 @@ Future<String> _getMethodSourceForInvocation(
181235
}
182236
}
183237
// prepare edits to replace conflicting variables
184-
var conflictingNames = _getNamesConflictingAt(contextNode);
185238
part._variables.forEach((VariableElement variable, List<SourceRange> ranges) {
186239
var originalName = variable.displayName;
187240
// prepare unique name
188-
String uniqueName;
189-
{
190-
uniqueName = originalName;
191-
var uniqueIndex = 2;
192-
while (conflictingNames.contains(uniqueName)) {
193-
uniqueName = originalName + uniqueIndex.toString();
194-
uniqueIndex++;
195-
}
196-
}
241+
var uniqueName = _getUniqueName(originalName, conflictingNames);
197242
// update references, if name was change
198243
if (uniqueName != originalName) {
199244
for (var range in ranges) {
@@ -203,7 +248,8 @@ Future<String> _getMethodSourceForInvocation(
203248
});
204249
// prepare source with applied arguments
205250
edits.sort((SourceEdit a, SourceEdit b) => b.offset - a.offset);
206-
return SourceEdit.applySequence(part._source, edits);
251+
var source = SourceEdit.applySequence(part._source, edits);
252+
return _InlineMethodResult(source, variableDeclarations);
207253
}
208254

209255
/// Returns the names which will shadow or will be shadowed by any declaration
@@ -243,6 +289,43 @@ Set<String> _getNamesConflictingAt(AstNode node) {
243289
return result;
244290
}
245291

292+
/// Returns a unique name that can be used instead of [originalName] by
293+
/// appending a number.
294+
String _getUniqueName(String originalName, Set<String> conflictingNames) {
295+
var uniqueName = originalName;
296+
var uniqueIndex = 2;
297+
while (conflictingNames.contains(uniqueName)) {
298+
uniqueName = originalName + uniqueIndex.toString();
299+
uniqueIndex++;
300+
}
301+
return uniqueName;
302+
}
303+
304+
/// Returns whether the use of this expression should be assigned to a variable
305+
/// to avoid potential side-effects of evaluating the expression multiple times
306+
/// or re-ordering by inlining further down the code.
307+
bool _shouldIntroduceVariable(Expression? expression, int occurrenceCount) {
308+
// If it's already a variable, never introduce a new one.
309+
if (expression?.canonicalElement is VariableElement) {
310+
return false;
311+
}
312+
// If it's a literal, introduce a variable if it's used more than once.
313+
if (expression is Literal) {
314+
return occurrenceCount > 1;
315+
}
316+
// If it's parenthesized, check the expression.
317+
if (expression is ParenthesizedExpression) {
318+
return _shouldIntroduceVariable(expression.expression, occurrenceCount);
319+
}
320+
// If it's a binary expression, check if either side needs a variable.
321+
if (expression is BinaryExpression) {
322+
return _shouldIntroduceVariable(expression.leftOperand, occurrenceCount) ||
323+
_shouldIntroduceVariable(expression.rightOperand, occurrenceCount);
324+
}
325+
// Otherwise, introduce a variable unless there was no expression.
326+
return expression != null;
327+
}
328+
246329
/// [InlineMethodRefactoring] implementation.
247330
class InlineMethodRefactoringImpl extends RefactoringImpl
248331
implements InlineMethodRefactoring {
@@ -270,6 +353,13 @@ class InlineMethodRefactoringImpl extends RefactoringImpl
270353
final List<_ReferenceProcessor> _referenceProcessors = [];
271354
final Set<Element> _alreadyMadeAsync = <Element>{};
272355

356+
/// Tracks variable names being introduced by the block they're being added
357+
/// to.
358+
///
359+
/// Used to prevent reusing the same new variable names when inlinine multiple
360+
/// calls into the same block.
361+
final Map<AstNode, Set<String>> _introducedVariablesByBlock = {};
362+
273363
InlineMethodRefactoringImpl(this.searchEngine, this.unitResult, this.offset)
274364
: sessionHelper = AnalysisSessionHelper(unitResult.session),
275365
utils = CorrectionUtils(unitResult);
@@ -561,6 +651,15 @@ class InlineMethodRefactoringImpl extends RefactoringImpl
561651
}
562652
}
563653

654+
/// The result of inlining a method is a combination of new code to inline and
655+
/// a set of variables to be declared.
656+
class _InlineMethodResult {
657+
final String source;
658+
final List<_VariableDeclaration> variableDeclarations;
659+
660+
_InlineMethodResult(this.source, this.variableDeclarations);
661+
}
662+
564663
class _ParameterOccurrence {
565664
final int baseOffset;
566665
final SimpleIdentifier identifier;
@@ -677,10 +776,29 @@ class _ReferenceProcessor {
677776
}
678777
// can we inline method body into "methodUsage" block?
679778
if (_canInlineBody(usage)) {
779+
// Get any variables we're previously introduced in this scope so we don't
780+
// redeclare the same name.
781+
var surroundingBlock = usage.thisOrAncestorMatching(
782+
(node) => node is Block || node is LibraryFragment,
783+
);
784+
var introducedVariableNames = surroundingBlock != null
785+
? ref._introducedVariablesByBlock.putIfAbsent(
786+
surroundingBlock,
787+
() => {},
788+
)
789+
// Types allow for no block but in practice we should never hit this
790+
// so just track into an empty list.
791+
: <String>{};
792+
793+
// Track which parameters have been assigned to which variables, so we
794+
// can reuse the same variable when the same parameter appears in both
795+
// the statements part and the expression part.
796+
var parameterToVariableName = <FormalParameterElement, String>{};
797+
680798
// insert non-return statements
681799
if (ref._methodStatementsPart != null) {
682800
// prepare statements source for invocation
683-
var source = await _getMethodSourceForInvocation(
801+
var result = await _getMethodSourceForInvocation(
684802
status,
685803
ChangeBuilder(
686804
session: ref.sessionHelper.session,
@@ -694,9 +812,19 @@ class _ReferenceProcessor {
694812
usage,
695813
target,
696814
arguments,
815+
introducedVariableNames,
816+
parameterToVariableName,
697817
);
698-
source = _refUtils.replaceSourceIndent(
699-
source,
818+
819+
// If there are variable declarations, insert them before the statement.
820+
_insertVariableDeclarations(
821+
result.variableDeclarations,
822+
introducedVariableNames,
823+
_refLineRange,
824+
);
825+
826+
var source = _refUtils.replaceSourceIndent(
827+
result.source,
700828
ref._methodStatementsPart!._prefix,
701829
_refPrefix,
702830
includeLeading: true,
@@ -712,7 +840,7 @@ class _ReferenceProcessor {
712840
// replace invocation with return expression
713841
if (ref._methodExpressionPart != null) {
714842
// prepare expression source for invocation
715-
var source = await _getMethodSourceForInvocation(
843+
var result = await _getMethodSourceForInvocation(
716844
status,
717845
ChangeBuilder(
718846
session: ref.sessionHelper.session,
@@ -726,8 +854,19 @@ class _ReferenceProcessor {
726854
usage,
727855
target,
728856
arguments,
857+
introducedVariableNames,
858+
parameterToVariableName,
859+
);
860+
861+
// If there are variable declarations, insert them before the statement.
862+
_insertVariableDeclarations(
863+
result.variableDeclarations,
864+
introducedVariableNames,
865+
_refLineRange,
729866
);
730867

868+
var source = result.source;
869+
731870
// If we inline the method expression into a string interpolation,
732871
// and the expression is not a single identifier, wrap it into `{}`.
733872
AstNode nodeToReplace = usage;
@@ -786,6 +925,30 @@ class _ReferenceProcessor {
786925
_addRefEdit(edit);
787926
}
788927

928+
/// Inserts [variableDeclarations] in front of the code at [refLineRange].
929+
void _insertVariableDeclarations(
930+
List<_VariableDeclaration> variableDeclarations,
931+
Set<String> introducedVariableNames,
932+
SourceRange? refLineRange,
933+
) {
934+
if (variableDeclarations.isEmpty) return;
935+
936+
var offset = _refLineRange?.offset ?? _node.offset;
937+
var insertOffset = _refUtils.getLineThis(offset);
938+
939+
var buffer = StringBuffer();
940+
for (var variable in variableDeclarations) {
941+
// Record the name as used so we don't reuse it at other call sites in the
942+
// block
943+
introducedVariableNames.add(variable.name);
944+
945+
buffer.write(
946+
'${_refPrefix}var ${variable.name} = ${variable.initializer};${_refUtils.endOfLine}',
947+
);
948+
}
949+
_addRefEdit(SourceEdit(insertOffset, 0, buffer.toString()));
950+
}
951+
789952
Future<void> _process(RefactoringStatus status) async {
790953
var nodeParent = _node.parent;
791954
// may be only single place should be inlined
@@ -1049,6 +1212,14 @@ class _SourcePart {
10491212
}
10501213
}
10511214

1215+
/// Represents a variable that needs to be declared before inlining.
1216+
class _VariableDeclaration {
1217+
final String name;
1218+
final String initializer;
1219+
1220+
_VariableDeclaration(this.name, this.initializer);
1221+
}
1222+
10521223
/// A visitor that fills [_SourcePart] with fields, parameters and variables.
10531224
class _VariablesVisitor extends GeneralizingAstVisitor<void> {
10541225
/// The [ExecutableElement] being inlined.

0 commit comments

Comments
 (0)