blob: f6a8db3ea453320297a150d1c9129eec19d0ff50 [file] [log] [blame]
// Copyright (c) 2020, 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/services/correction/fix.dart';
import 'package:analyzer/dart/ast/ast.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/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
class ConvertToSetLiteral extends CorrectionProducer {
@override
AssistKind get assistKind => DartAssistKind.CONVERT_TO_SET_LITERAL;
@override
FixKind get fixKind => DartFixKind.CONVERT_TO_SET_LITERAL;
@override
Future<void> compute(ChangeBuilder builder) async {
//
// Check whether this is an invocation of `toSet` on a list literal.
//
var invocation = _findInvocationOfToSet();
if (invocation != null) {
//
// Extract the information needed to build the edit.
//
var target = invocation.target as ListLiteral;
var hasTypeArgs = target.typeArguments != null;
var openRange = range.token(target.leftBracket);
var closeRange = range.startEnd(target.rightBracket, invocation);
//
// Build the change and return the assist.
//
await builder.addDartFileEdit(file, (builder) {
if (hasTypeArgs || _listHasUnambiguousElement(target)) {
builder.addSimpleReplacement(openRange, '{');
} else {
builder.addSimpleReplacement(openRange, '<dynamic>{');
}
builder.addSimpleReplacement(closeRange, '}');
});
return;
}
//
// Check whether this is one of the constructors defined on `Set`.
//
var creation = _findSetCreation();
if (creation != null) {
//
// Extract the information needed to build the edit.
//
var name = creation.constructorName.name;
var constructorTypeArguments =
creation.constructorName.type.typeArguments;
TypeArgumentList elementTypeArguments;
SourceRange elementsRange;
if (name == null) {
// Handle an invocation of the default constructor `Set()`.
} else if (name.name == 'from' || name.name == 'of') {
// Handle an invocation of the constructor `Set.from()` or `Set.of()`.
var arguments = creation.argumentList.arguments;
if (arguments.length != 1) {
return;
}
if (arguments[0] is ListLiteral) {
var elements = arguments[0] as ListLiteral;
elementTypeArguments = elements.typeArguments;
elementsRange =
range.endStart(elements.leftBracket, elements.rightBracket);
} else {
// TODO(brianwilkerson) Consider handling other iterables. Literal
// sets could be treated like lists, and arbitrary iterables by using
// a spread.
return;
}
} else {
// Invocation of an unhandled constructor.
return;
}
//
// Build the edit.
//
await builder.addDartFileEdit(file, (builder) {
builder.addReplacement(range.node(creation), (builder) {
if (constructorTypeArguments != null) {
builder.write(utils.getNodeText(constructorTypeArguments));
} else if (elementTypeArguments != null) {
builder.write(utils.getNodeText(elementTypeArguments));
} else if (!_setWouldBeInferred(creation)) {
builder.write('<dynamic>');
}
builder.write('{');
if (elementsRange != null) {
builder.write(utils.getRangeText(elementsRange));
}
builder.write('}');
});
});
}
}
/// Return the invocation of `List.toSet` that is to be converted, or `null`
/// if the cursor is not inside a invocation of `List.toSet`.
MethodInvocation _findInvocationOfToSet() {
var invocation = node.thisOrAncestorOfType<MethodInvocation>();
if (invocation == null ||
node.offset > invocation.argumentList.offset ||
invocation.methodName.name != 'toSet' ||
invocation.target is! ListLiteral) {
return null;
}
return invocation;
}
/// Return the invocation of a `Set` constructor that is to be converted, or
/// `null` if the cursor is not inside the invocation of a constructor.
InstanceCreationExpression _findSetCreation() {
var creation = node.thisOrAncestorOfType<InstanceCreationExpression>();
// TODO(brianwilkerson) Consider also accepting uses of LinkedHashSet.
if (creation == null ||
node.offset > creation.argumentList.offset ||
creation.staticType.element != typeProvider.setElement) {
return null;
}
return creation;
}
/// Return `true` if the instance [creation] contains at least one unambiguous
/// element that would cause a set to be inferred.
bool _hasUnambiguousElement(InstanceCreationExpression creation) {
var arguments = creation.argumentList.arguments;
if (arguments == null || arguments.isEmpty) {
return false;
}
return _listHasUnambiguousElement(arguments[0]);
}
/// Return `true` if the [element] is sufficient to lexically make the
/// enclosing literal a set literal rather than a map.
bool _isUnambiguousElement(CollectionElement element) {
if (element is ForElement) {
return _isUnambiguousElement(element.body);
} else if (element is IfElement) {
return _isUnambiguousElement(element.thenElement) ||
_isUnambiguousElement(element.elseElement);
} else if (element is Expression) {
return true;
}
return false;
}
/// Return `true` if the given [node] is a list literal whose elements, if
/// placed inside curly braces, would lexically make the resulting literal a
/// set literal rather than a map literal.
bool _listHasUnambiguousElement(AstNode node) {
if (node is ListLiteral && node.elements.isNotEmpty) {
for (var element in node.elements) {
if (_isUnambiguousElement(element)) {
return true;
}
}
}
return false;
}
/// Return `true` if a set would be inferred if the literal replacing the
/// instance [creation] did not have explicit type arguments.
bool _setWouldBeInferred(InstanceCreationExpression creation) {
var parent = creation.parent;
if (parent is VariableDeclaration) {
var parent2 = parent.parent;
if (parent2 is VariableDeclarationList &&
parent2.type?.type?.element == typeProvider.setElement) {
return true;
}
} else if (parent.parent is InvocationExpression) {
var parameterElement = creation.staticParameterElement;
if (parameterElement?.type?.element == typeProvider.setElement) {
return true;
}
}
return _hasUnambiguousElement(creation);
}
/// Return an instance of this class. Used as a tear-off in `FixProcessor`.
static ConvertToSetLiteral newInstance() => ConvertToSetLiteral();
}