blob: 706d0b440a3f47204d8b18ea3b0f1a01f50333d2 [file] [log] [blame]
// 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/dart/abstract_producer.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
class ExtractLocalVariable extends CorrectionProducer {
@override
FixKind get fixKind => DartFixKind.EXTRACT_LOCAL_VARIABLE;
@override
Future<void> compute(ChangeBuilder builder) async {
final node = this.node;
if (node is! SimpleIdentifier) {
return;
}
var parent = node.parent;
if (parent is MethodInvocation && parent.methodName == node) {
await _rewrite(
builder: builder,
target: parent.target,
);
}
if (parent is PrefixedIdentifier && parent.identifier == node) {
await _rewrite(
builder: builder,
target: parent.prefix,
);
}
if (parent is PropertyAccess && parent.propertyName == node) {
await _rewrite(
builder: builder,
target: parent.target,
);
}
}
Future<void> _rewrite({
required ChangeBuilder builder,
required Expression? target,
}) async {
if (target is PrefixedIdentifier) {
await _rewriteProperty(
builder: builder,
target: target,
targetProperty: target.staticElement,
);
}
if (target is PropertyAccess) {
await _rewriteProperty(
builder: builder,
target: target,
targetProperty: target.propertyName.staticElement,
);
}
if (target is SimpleIdentifier) {
await _rewriteProperty(
builder: builder,
target: target,
targetProperty: target.staticElement,
);
}
}
Future<void> _rewriteProperty({
required ChangeBuilder builder,
required Expression target,
required Element? targetProperty,
}) async {
if (targetProperty is PropertyAccessorElement &&
targetProperty.isGetter &&
typeSystem.isPotentiallyNullable(targetProperty.returnType)) {
AstNode? enclosingNode = target;
while (true) {
if (enclosingNode == null || enclosingNode is FunctionBody) {
return;
}
if (enclosingNode is IfStatement) {
var condition = enclosingNode.condition;
if (condition is BinaryExpression &&
condition.rightOperand is NullLiteral &&
condition.operator.type == TokenType.BANG_EQ) {
var encoder = _ExpressionEncoder();
var leftCode = encoder.encode(condition.leftOperand);
var targetCode = encoder.encode(target);
if (leftCode == targetCode) {
var occurrences = <SourceRange>[];
enclosingNode.accept(
_OccurrencesVisitor(encoder, occurrences, leftCode),
);
var ifOffset = enclosingNode.offset;
var ifLineOffset = utils.getLineContentStart(ifOffset);
var prefix = utils.getLinePrefix(ifOffset);
var initializerCode = utils.getNodeText(target);
if (target is SimpleIdentifier) {
initializerCode = 'this.$initializerCode';
}
await builder.addDartFileEdit(file, (builder) {
var propertyName = targetProperty.name;
builder.addInsertion(ifLineOffset, (builder) {
builder.write(prefix);
builder.writeln('final $propertyName = $initializerCode;');
});
for (var occurrence in occurrences) {
builder.addSimpleReplacement(occurrence, propertyName);
}
});
return;
}
}
break;
}
enclosingNode = enclosingNode.parent;
}
}
}
}
class _ExpressionEncoder {
final Map<Element, int> _elementIds = {};
String encode(Expression node) {
var tokens = TokenUtils.getNodeTokens(node);
var tokenToElementMap = Map<Token, Element>.identity();
node.accept(
_FunctionAstVisitor(
simpleIdentifier: (node) {
var element = node.staticElement;
if (element != null) {
tokenToElementMap[node.token] = element;
}
},
),
);
var tokensWithId = tokens.map((token) {
var tokenString = token.lexeme;
var element = tokenToElementMap[token];
if (element != null) {
var elementId = _elementIds.putIfAbsent(
element,
() => _elementIds.length,
);
tokenString += '#$elementId';
}
return tokenString;
});
const separator = '\uFFFF';
return tokensWithId.join(separator) + separator;
}
}
/// [RecursiveAstVisitor] that delegates visit methods to functions.
class _FunctionAstVisitor extends RecursiveAstVisitor<void> {
final void Function(SimpleIdentifier)? simpleIdentifier;
_FunctionAstVisitor({
this.simpleIdentifier,
});
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (simpleIdentifier != null) {
simpleIdentifier!(node);
}
super.visitSimpleIdentifier(node);
}
}
class _OccurrencesVisitor extends GeneralizingAstVisitor<void> {
final _ExpressionEncoder encoder;
final List<SourceRange> occurrences;
final String searchCode;
_OccurrencesVisitor(this.encoder, this.occurrences, this.searchCode);
@override
void visitExpression(Expression node) {
var nodeCode = encoder.encode(node);
if (nodeCode == searchCode) {
occurrences.add(range.node(node));
}
super.visitExpression(node);
}
}