blob: 8f04696f5768bc8a06f5a6a78caa039b83a4505a [file] [log] [blame]
// Copyright (c) 2021, 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:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:collection/collection.dart';
class AddKeyToConstructors extends CorrectionProducer {
@override
FixKind get fixKind => DartFixKind.ADD_KEY_TO_CONSTRUCTORS;
@override
Future<void> compute(ChangeBuilder builder) async {
var node = this.node;
var parent = node.parent;
if (node is SimpleIdentifier && parent is ClassDeclaration) {
// The lint is on the name of the class when there are no constructors.
var targetLocation =
utils.prepareNewConstructorLocation(resolvedResult.session, parent);
if (targetLocation == null) {
return;
}
var keyType = await _getKeyType();
if (keyType == null) {
return;
}
var className = node.name;
var constructors = parent.declaredElement?.supertype?.constructors;
if (constructors == null) {
return;
}
var canBeConst = _canBeConst(parent, constructors);
await builder.addDartFileEdit(file, (builder) {
builder.addInsertion(targetLocation.offset, (builder) {
builder.write(targetLocation.prefix);
if (canBeConst) {
builder.write('const ');
}
builder.write(className);
builder.write('({');
if (libraryElement.featureSet.isEnabled(Feature.super_parameters)) {
builder.write('super.key});');
} else {
builder.writeType(keyType);
builder.write(' key}) : super(key: key);');
}
builder.write(targetLocation.suffix);
});
});
} else if (parent is ConstructorDeclaration) {
// The lint is on a constructor when that constructor doesn't have a `key`
// parameter.
var keyType = await _getKeyType();
if (keyType == null) {
return;
}
var superParameters =
libraryElement.featureSet.isEnabled(Feature.super_parameters);
void writeKey(DartEditBuilder builder) {
if (superParameters) {
builder.write('super.key');
} else {
builder.writeType(keyType);
builder.write(' key');
}
}
var parameterList = parent.parameters;
var parameters = parameterList.parameters;
if (parameters.isEmpty) {
// There are no parameters, so add the first parameter.
await builder.addDartFileEdit(file, (builder) {
builder.addInsertion(parameterList.leftParenthesis.end, (builder) {
builder.write('{');
writeKey(builder);
builder.write('}');
});
_updateSuper(builder, parent, superParameters);
});
return;
}
var leftDelimiter = parameterList.leftDelimiter;
if (leftDelimiter == null) {
// There are no named parameters, so add the delimiters.
await builder.addDartFileEdit(file, (builder) {
builder.addInsertion(parameters.last.end, (builder) {
builder.write(', {');
writeKey(builder);
builder.write('}');
});
_updateSuper(builder, parent, superParameters);
});
} else if (leftDelimiter.type == TokenType.OPEN_CURLY_BRACKET) {
// There are other named parameters, so add the new named parameter.
await builder.addDartFileEdit(file, (builder) {
builder.addInsertion(leftDelimiter.end, (builder) {
writeKey(builder);
builder.write(', ');
});
_updateSuper(builder, parent, superParameters);
});
}
}
}
/// Return `true` if the [classDeclaration] can be instantiated as a `const`.
bool _canBeConst(ClassDeclaration classDeclaration,
List<ConstructorElement> constructors) {
for (var constructor in constructors) {
if (constructor.isDefaultConstructor && !constructor.isConst) {
return false;
}
}
for (var member in classDeclaration.members) {
if (member is FieldDeclaration && !member.isStatic) {
if (!member.fields.isFinal) {
return false;
}
for (var variableDeclaration in member.fields.variables) {
var initializer = variableDeclaration.initializer;
if (initializer is InstanceCreationExpression &&
!initializer.isConst) {
return false;
}
}
}
}
return true;
}
/// Return the type for the class `Key`.
Future<DartType?> _getKeyType() async {
var keyClass = await sessionHelper.getClass(flutter.widgetsUri, 'Key');
if (keyClass == null) {
return null;
}
var isNonNullable = resolvedResult.libraryElement.featureSet
.isEnabled(Feature.non_nullable);
return keyClass.instantiate(
typeArguments: const [],
nullabilitySuffix:
isNonNullable ? NullabilitySuffix.question : NullabilitySuffix.star,
);
}
void _updateSuper(DartFileEditBuilder builder,
ConstructorDeclaration constructor, bool superParameters) {
if (constructor.factoryKeyword != null ||
constructor.redirectedConstructor != null) {
// Can't have a super constructor invocation.
// TODO(brianwilkerson) Consider extending the redirected constructor to
// also take a key, or finding the constructor invocation in the body of
// the factory and updating it.
return;
}
var initializers = constructor.initializers;
SuperConstructorInvocation? invocation;
for (var initializer in initializers) {
if (initializer is SuperConstructorInvocation) {
invocation = initializer;
} else if (initializer is RedirectingConstructorInvocation) {
return;
}
}
if (superParameters) {
if (invocation != null && invocation.argumentList.arguments.isEmpty) {
var previous = initializers.length == 1
? constructor.parameters
: initializers[initializers.indexOf(invocation) - 1];
builder.addDeletion(range.endStart(previous, constructor.body));
}
return;
}
if (invocation == null) {
// There is no super constructor invocation, so add one.
if (initializers.isEmpty) {
builder.addSimpleInsertion(
constructor.parameters.rightParenthesis.end, ' : super(key: key)');
} else {
builder.addSimpleInsertion(initializers.last.end, ', super(key: key)');
}
} else {
// There is a super constructor invocation, so update it.
var argumentList = invocation.argumentList;
var arguments = argumentList.arguments;
var existing = arguments.firstWhereOrNull((argument) =>
argument is NamedExpression && argument.name.label.name == 'key');
if (existing == null) {
// There is no 'key' argument, so add it.
var namedArguments = arguments.whereType<NamedExpression>();
var firstNamed = namedArguments.firstOrNull;
var token = firstNamed?.beginToken ?? argumentList.endToken;
var comma = token.previous?.type == TokenType.COMMA;
builder.addInsertion(token.offset, (builder) {
if (arguments.length != namedArguments.length) {
// there are unnamed arguments
if (!comma) {
builder.write(',');
}
builder.write(' ');
}
builder.write('key: key');
if (firstNamed != null) {
builder.write(', ');
} else if (comma) {
builder.write(',');
}
});
} else {
// There is an existing 'key' argument, so we leave it alone.
}
}
}
}