blob: b32a6226c3d86cf5d08a1f41df5cc706f2fa38c3 [file] [log] [blame]
// Copyright (c) 2019, 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:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_algebra.dart';
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/src/nullability_node.dart';
import 'package:nnbd_migration/src/nullability_node_target.dart';
/// Representation of a type in the code to be migrated. In addition to
/// tracking the (unmigrated) [DartType], we track the [ConstraintVariable]s
/// indicating whether the type, and the types that compose it, are nullable.
class DecoratedType implements DecoratedTypeInfo {
@override
final DartType? type;
@override
final NullabilityNode? node;
@override
final DecoratedType? returnType;
/// If `this` is a function type, the [DecoratedType] of each of its
/// positional parameters (including both required and optional positional
/// parameters).
final List<DecoratedType?>? positionalParameters;
/// If `this` is a function type, the [DecoratedType] of each of its named
/// parameters.
final Map<String, DecoratedType?>? namedParameters;
/// If `this` is a parameterized type, the [DecoratedType] of each of its
/// type parameters.
///
/// TODO(paulberry): how should we handle generic typedefs?
final List<DecoratedType?> typeArguments;
DecoratedType(this.type, this.node,
{this.returnType,
this.positionalParameters = const [],
this.namedParameters = const {},
this.typeArguments = const []}) {
assert(() {
assert(node != null);
var type = this.type;
if (type is InterfaceType) {
assert(returnType == null);
assert(positionalParameters!.isEmpty);
assert(namedParameters!.isEmpty);
assert(typeArguments.length == type.typeArguments.length);
for (int i = 0; i < typeArguments.length; i++) {
assert(typeArguments[i]!.type == type.typeArguments[i],
'${typeArguments[i]!.type} != ${type.typeArguments[i]}');
}
} else if (type is FunctionType) {
assert(returnType!.type == type.returnType);
int positionalParameterCount = 0;
int namedParameterCount = 0;
for (var parameter in type.parameters) {
if (parameter.isNamed) {
assert(namedParameters![parameter.name]!.type == parameter.type);
namedParameterCount++;
} else {
assert(positionalParameters![positionalParameterCount]!.type ==
parameter.type);
positionalParameterCount++;
}
}
assert(positionalParameters!.length == positionalParameterCount);
assert(namedParameters!.length == namedParameterCount);
assert(typeArguments.isEmpty);
} else if (node is TypeParameterType) {
assert(returnType == null);
assert(positionalParameters!.isEmpty);
assert(namedParameters!.isEmpty);
assert(typeArguments.isEmpty);
} else {
assert(returnType == null);
assert(positionalParameters!.isEmpty);
assert(namedParameters!.isEmpty);
assert(typeArguments.isEmpty);
}
return true;
}());
}
/// Creates a decorated type corresponding to [type], with fresh nullability
/// nodes everywhere that don't correspond to any source location. These
/// nodes can later be unioned with other nodes.
factory DecoratedType.forImplicitFunction(
TypeProvider typeProvider,
FunctionType type,
NullabilityNode node,
NullabilityGraph graph,
NullabilityNodeTarget target,
{DecoratedType? returnType}) {
var positionalParameters = <DecoratedType>[];
var namedParameters = <String, DecoratedType>{};
int index = 0;
for (var parameter in type.parameters) {
if (parameter.isPositional) {
positionalParameters.add(DecoratedType.forImplicitType(typeProvider,
parameter.type, graph, target.positionalParameter(index++)));
} else {
var name = parameter.name;
namedParameters[name] = DecoratedType.forImplicitType(
typeProvider, parameter.type, graph, target.namedParameter(name));
}
}
for (var element in type.typeFormals) {
if (DecoratedTypeParameterBounds.current!.get(element) == null) {
DecoratedTypeParameterBounds.current!.put(
element,
DecoratedType.forImplicitType(
typeProvider,
element.bound ?? typeProvider.objectType,
graph,
target.typeFormalBound(element.name)));
}
}
return DecoratedType(type, node,
returnType: returnType ??
DecoratedType.forImplicitType(
typeProvider, type.returnType, graph, target.returnType()),
namedParameters: namedParameters,
positionalParameters: positionalParameters);
}
/// Creates a DecoratedType corresponding to [type], with fresh nullability
/// nodes everywhere that don't correspond to any source location. These
/// nodes can later be unioned with other nodes.
factory DecoratedType.forImplicitType(TypeProvider typeProvider,
DartType? type, NullabilityGraph graph, NullabilityNodeTarget target,
{List<DecoratedType?>? typeArguments}) {
var nullabilityNode = NullabilityNode.forInferredType(target);
if (type is InterfaceType) {
assert(() {
if (typeArguments != null) {
assert(typeArguments.length == type.typeArguments.length);
for (var i = 0; i < typeArguments.length; ++i) {
assert(typeArguments[i]!.type == type.typeArguments[i]);
}
}
return true;
}());
int index = 0;
typeArguments ??= type.typeArguments
.map((t) => DecoratedType.forImplicitType(
typeProvider, t, graph, target.typeArgument(index++)))
.toList();
return DecoratedType(type, nullabilityNode, typeArguments: typeArguments);
} else if (type is FunctionType) {
if (typeArguments != null) {
throw 'Not supported: implicit function type with explicit type '
'arguments';
}
return DecoratedType.forImplicitFunction(
typeProvider, type, nullabilityNode, graph, target);
} else {
assert(typeArguments == null);
return DecoratedType(type, nullabilityNode);
}
}
/// Creates a [DecoratedType] for a synthetic type parameter, to be used
/// during comparison of generic function types.
DecoratedType._forTypeParameterSubstitution(TypeParameterElement parameter)
: type = TypeParameterTypeImpl(
element: parameter,
nullabilitySuffix: NullabilitySuffix.star,
),
node = null,
returnType = null,
positionalParameters = const [],
namedParameters = const {},
typeArguments = const [] {
// We'll be storing the type parameter bounds in
// [_decoratedTypeParameterBounds] so the type parameter needs to have an
// enclosing element of `null`.
assert(parameter.enclosingElement == null,
'$parameter should not have parent ${parameter.enclosingElement}');
}
/// If `this` represents an interface type, returns the substitution necessary
/// to produce this type using the class's type as a starting point.
/// Otherwise throws an exception.
///
/// For instance, if `this` represents `List<int?1>`, returns the substitution
/// `{T: int?1}`, where `T` is the [TypeParameterElement] for `List`'s type
/// parameter.
Map<TypeParameterElement, DecoratedType?> get asSubstitution {
var type = this.type;
if (type is InterfaceType) {
return Map<TypeParameterElement, DecoratedType?>.fromIterables(
type.element.typeParameters, typeArguments);
} else {
throw StateError(
'Tried to convert a non-interface type to a substitution');
}
}
/// If this type is a function type, returns its generic formal parameters.
/// Otherwise returns `null`.
List<TypeParameterElement>? get typeFormals {
var type = this.type;
if (type is FunctionType) {
return type.typeFormals;
} else {
return null;
}
}
@override
bool operator ==(Object other) {
if (other is DecoratedType) {
if (!identical(node, other.node)) return false;
var thisType = type;
var otherType = other.type;
if (thisType is FunctionType && otherType is FunctionType) {
if (thisType.normalParameterTypes.length !=
otherType.normalParameterTypes.length) {
return false;
}
if (thisType.typeFormals.length != otherType.typeFormals.length) {
return false;
}
var renamed = RenamedDecoratedFunctionTypes.match(
this, other, (bound1, bound2) => bound1 == bound2);
if (renamed == null) return false;
if (renamed.returnType1 != renamed.returnType2) return false;
if (!_compareLists(
renamed.positionalParameters1, renamed.positionalParameters2)) {
return false;
}
if (!_compareMaps(renamed.namedParameters1, renamed.namedParameters2)) {
return false;
}
return true;
} else if (thisType is InterfaceType && otherType is InterfaceType) {
if (thisType.element != otherType.element) return false;
if (!_compareLists(typeArguments, other.typeArguments)) {
return false;
}
return true;
} else {
return thisType == otherType;
}
}
return false;
}
/// Converts one function type into another by substituting the given
/// [argumentTypes] for the function's generic parameters.
DecoratedType instantiate(List<DecoratedType> argumentTypes) {
var type = this.type as FunctionType;
var typeFormals = type.typeFormals;
assert(argumentTypes.length == typeFormals.length);
List<DartType> undecoratedArgumentTypes = [];
Map<TypeParameterElement, DecoratedType> substitution = {};
for (int i = 0; i < argumentTypes.length; i++) {
var argumentType = argumentTypes[i];
undecoratedArgumentTypes.add(argumentType.type!);
substitution[typeFormals[i]] = argumentType;
}
return _substituteFunctionAfterFormals(
type.instantiate(undecoratedArgumentTypes), substitution);
}
@override
DecoratedTypeInfo? namedParameter(String name) => namedParameters![name];
@override
DecoratedTypeInfo? positionalParameter(int i) => positionalParameters![i];
/// Updates the [roles] map with information about the nullability nodes
/// pointed to by this decorated type.
///
/// Each entry stored in [roles] maps the role of the node to the node itself.
/// Roles look like pathnames, where each path component is an integer to
/// represent a type argument (or a positional parameter type, in the case of
/// a function type), an name to represent a named parameter type, or `@r` to
/// represent a return type.
void recordRoles(Map<String, NullabilityNode?> roles,
{String rolePrefix = ''}) {
roles[rolePrefix] = node;
returnType?.recordRoles(roles, rolePrefix: '$rolePrefix/@r');
for (int i = 0; i < positionalParameters!.length; i++) {
positionalParameters![i]!
.recordRoles(roles, rolePrefix: '$rolePrefix/$i');
}
for (var entry in namedParameters!.entries) {
entry.value!.recordRoles(roles, rolePrefix: '$rolePrefix/${entry.key}');
}
for (int i = 0; i < typeArguments.length; i++) {
typeArguments[i]!.recordRoles(roles, rolePrefix: '$rolePrefix/$i');
}
}
/// Apply the given [substitution] to this type.
///
/// [undecoratedResult] is the result of the substitution, as determined by
/// the normal type system. If not supplied, it is inferred.
DecoratedType substitute(
Map<TypeParameterElement, DecoratedType?> substitution,
[DartType? undecoratedResult]) {
if (substitution.isEmpty) return this;
if (undecoratedResult == null) {
var type = this.type!;
undecoratedResult = Substitution.fromPairs(
substitution.keys.toList(),
substitution.values.map((d) => d!.type!).toList(),
).substituteType(type);
if (undecoratedResult is FunctionType && type is FunctionType) {
for (int i = 0; i < undecoratedResult.typeFormals.length; i++) {
DecoratedTypeParameterBounds.current!.put(
undecoratedResult.typeFormals[i],
DecoratedTypeParameterBounds.current!.get(type.typeFormals[i]));
}
}
}
return _substitute(substitution, undecoratedResult);
}
@override
String toString() {
var trailing = node == null ? '' : node!.debugSuffix;
var type = this.type;
if (type is TypeParameterType || type is VoidType) {
return '$type$trailing';
} else if (type is InterfaceType) {
var name = type.element.name;
var args = '';
if (type.typeArguments.isNotEmpty) {
args = '<${typeArguments.join(', ')}>';
}
return '$name$args$trailing';
} else if (type is FunctionType) {
String formals = '';
if (type.typeFormals.isNotEmpty) {
formals = '<${type.typeFormals.join(', ')}>';
}
List<String> paramStrings = [];
for (int i = 0; i < positionalParameters!.length; i++) {
var prefix = '';
if (i == type.normalParameterTypes.length) {
prefix = '[';
}
paramStrings.add('$prefix${positionalParameters![i]}');
}
if (type.normalParameterTypes.length < positionalParameters!.length) {
paramStrings.last += ']';
}
if (namedParameters!.isNotEmpty) {
var prefix = '{';
for (var entry in namedParameters!.entries) {
paramStrings.add('$prefix${entry.key}: ${entry.value}');
prefix = '';
}
paramStrings.last += '}';
}
var args = paramStrings.join(', ');
return '$returnType Function$formals($args)$trailing';
} else if (type is DynamicTypeImpl) {
return 'dynamic';
} else if (type!.isBottom) {
return 'Never$trailing';
} else {
throw '$type'; // TODO(paulberry)
}
}
@override
DecoratedTypeInfo? typeArgument(int i) => typeArguments[i];
/// Creates a shallow copy of `this`, replacing the nullability node.
DecoratedType withNode(NullabilityNode? node) => DecoratedType(type, node,
returnType: returnType,
positionalParameters: positionalParameters,
namedParameters: namedParameters,
typeArguments: typeArguments);
/// Creates a shallow copy of `this`, replacing the nullability node and the
/// type.
DecoratedType withNodeAndType(NullabilityNode node, DartType? type) =>
DecoratedType(type, node,
returnType: returnType,
positionalParameters: positionalParameters,
namedParameters: namedParameters,
typeArguments: typeArguments);
/// Internal implementation of [_substitute], used as a recursion target.
DecoratedType _substitute(
Map<TypeParameterElement, DecoratedType?> substitution,
DartType? undecoratedResult) {
var type = this.type;
if (type is FunctionType && undecoratedResult is FunctionType) {
var typeFormals = type.typeFormals;
assert(typeFormals.length == undecoratedResult.typeFormals.length);
if (typeFormals.isNotEmpty) {
// The analyzer sometimes allocates fresh type variables when performing
// substitutions, so we need to reflect that in our decorations by
// substituting to use the type variables the analyzer used.
substitution =
Map<TypeParameterElement, DecoratedType>.from(substitution);
for (int i = 0; i < typeFormals.length; i++) {
// Check if it's a fresh type variable.
if (undecoratedResult.typeFormals[i].enclosingElement == null) {
substitution[typeFormals[i]] =
DecoratedType._forTypeParameterSubstitution(
undecoratedResult.typeFormals[i]);
}
}
for (int i = 0; i < typeFormals.length; i++) {
var typeFormal = typeFormals[i];
var oldDecoratedBound =
DecoratedTypeParameterBounds.current!.get(typeFormal);
var undecoratedResult2 = undecoratedResult.typeFormals[i].bound;
if (undecoratedResult2 == null) {
if (oldDecoratedBound == null) {
assert(
false, 'Could not find old decorated bound for type formal');
// Recover the best we can by assuming a bound of `dynamic`.
oldDecoratedBound = DecoratedType(
DynamicTypeImpl.instance,
NullabilityNode.forInferredType(
NullabilityNodeTarget.text('Type parameter bound')));
}
undecoratedResult2 = oldDecoratedBound.type;
}
var newDecoratedBound =
oldDecoratedBound!._substitute(substitution, undecoratedResult2);
if (identical(typeFormal, undecoratedResult.typeFormals[i])) {
assert(oldDecoratedBound == newDecoratedBound);
} else {
DecoratedTypeParameterBounds.current!
.put(typeFormal, newDecoratedBound);
}
}
}
return _substituteFunctionAfterFormals(undecoratedResult, substitution);
} else if (type is InterfaceType && undecoratedResult is InterfaceType) {
List<DecoratedType> newTypeArguments = [];
for (int i = 0; i < typeArguments.length; i++) {
newTypeArguments.add(typeArguments[i]!
.substitute(substitution, undecoratedResult.typeArguments[i]));
}
return DecoratedType(undecoratedResult, node,
typeArguments: newTypeArguments);
} else if (type is TypeParameterType) {
var inner = substitution[type.element];
if (inner == null) {
return this;
} else {
return inner.withNodeAndType(
NullabilityNode.forSubstitution(inner.node, node),
undecoratedResult);
}
} else if (type!.isVoid || type.isDynamic) {
return this;
}
throw '$type.substitute($type | $substitution)'; // TODO(paulberry)
}
/// Performs the logic that is common to substitution and function type
/// instantiation. Namely, a decorated type is formed whose undecorated type
/// is [undecoratedResult], and whose return type, positional parameters, and
/// named parameters are formed by performing the given [substitution].
DecoratedType _substituteFunctionAfterFormals(FunctionType undecoratedResult,
Map<TypeParameterElement, DecoratedType?> substitution) {
var newPositionalParameters = <DecoratedType>[];
var numRequiredParameters = undecoratedResult.normalParameterTypes.length;
for (int i = 0; i < positionalParameters!.length; i++) {
var undecoratedParameterType = i < numRequiredParameters
? undecoratedResult.normalParameterTypes[i]
: undecoratedResult.optionalParameterTypes[i - numRequiredParameters];
newPositionalParameters.add(positionalParameters![i]!
._substitute(substitution, undecoratedParameterType));
}
var newNamedParameters = <String, DecoratedType>{};
for (var entry in namedParameters!.entries) {
var name = entry.key;
var undecoratedParameterType =
undecoratedResult.namedParameterTypes[name];
newNamedParameters[name] =
(entry.value!._substitute(substitution, undecoratedParameterType));
}
return DecoratedType(undecoratedResult, node,
returnType:
returnType!._substitute(substitution, undecoratedResult.returnType),
positionalParameters: newPositionalParameters,
namedParameters: newNamedParameters);
}
static bool _compareLists(
List<DecoratedType?>? list1, List<DecoratedType?>? list2) {
if (identical(list1, list2)) return true;
if (list1!.length != list2!.length) return false;
for (int i = 0; i < list1.length; i++) {
if (list1[i] != list2[i]) return false;
}
return true;
}
static bool _compareMaps(
Map<String, DecoratedType?>? map1, Map<String, DecoratedType?>? map2) {
if (identical(map1, map2)) return true;
if (map1!.length != map2!.length) return false;
for (var entry in map1.entries) {
if (entry.value != map2[entry.key]) return false;
}
return true;
}
}
/// Data structure mapping type parameters to their decorated bounds.
///
/// Since we need to be able to access this mapping globally throughout the
/// migration engine, from places where we can't easily inject it, the current
/// mapping is stored in a static variable.
class DecoratedTypeParameterBounds {
/// The [DecoratedTypeParameterBounds] currently in use, or `null` if we are
/// not currently in a stage of migration where we need access to the
/// decorated types of type parameter bounds.
///
/// If `null`, then attempts to look up the decorated types of type parameter
/// bounds will fail.
static DecoratedTypeParameterBounds? current;
final _orphanBounds = Expando<DecoratedType>();
final _parentedBounds = <TypeParameterElement, DecoratedType?>{};
DecoratedType? get(TypeParameterElement element) {
if (element.enclosingElement == null) {
return _orphanBounds[element];
} else {
return _parentedBounds[element];
}
}
void put(TypeParameterElement element, DecoratedType? bounds) {
if (element.enclosingElement == null) {
_orphanBounds[element] = bounds;
} else {
_parentedBounds[element] = bounds;
}
}
}
/// Helper class that renames the type parameters in two decorated function
/// types so that they match.
class RenamedDecoratedFunctionTypes {
final DecoratedType? returnType1;
final DecoratedType? returnType2;
final List<DecoratedType?>? positionalParameters1;
final List<DecoratedType?>? positionalParameters2;
final Map<String, DecoratedType?>? namedParameters1;
final Map<String, DecoratedType?>? namedParameters2;
RenamedDecoratedFunctionTypes._(
this.returnType1,
this.returnType2,
this.positionalParameters1,
this.positionalParameters2,
this.namedParameters1,
this.namedParameters2);
/// Attempt to find a renaming of the type parameters of [type1] and [type2]
/// (both of which should be function types) such that the generic type
/// parameters match.
///
/// The callback [boundsMatcher] is used to determine whether type parameter
/// bounds match.
///
/// If such a renaming can be found, it is returned. If not, `null` is
/// returned.
static RenamedDecoratedFunctionTypes? match(
DecoratedType type1,
DecoratedType type2,
bool Function(DecoratedType, DecoratedType) boundsMatcher) {
if (!_isNeeded(type1.typeFormals, type2.typeFormals)) {
return RenamedDecoratedFunctionTypes._(
type1.returnType,
type2.returnType,
type1.positionalParameters,
type2.positionalParameters,
type1.namedParameters,
type2.namedParameters);
}
// Create a fresh set of type variables and substitute so we can
// compare safely.
var substitution1 = <TypeParameterElement, DecoratedType>{};
var substitution2 = <TypeParameterElement, DecoratedType>{};
var newParameters = <TypeParameterElement>[];
for (int i = 0; i < type1.typeFormals!.length; i++) {
var newParameter =
TypeParameterElementImpl.synthetic(type1.typeFormals![i].name);
newParameters.add(newParameter);
var newParameterType =
DecoratedType._forTypeParameterSubstitution(newParameter);
substitution1[type1.typeFormals![i]] = newParameterType;
substitution2[type2.typeFormals![i]] = newParameterType;
}
for (int i = 0; i < type1.typeFormals!.length; i++) {
var bound1 = DecoratedTypeParameterBounds.current!
.get((type1.type as FunctionType).typeFormals[i])!
.substitute(substitution1);
var bound2 = DecoratedTypeParameterBounds.current!
.get((type2.type as FunctionType).typeFormals[i])!
.substitute(substitution2);
if (!boundsMatcher(bound1, bound2)) return null;
DecoratedTypeParameterBounds.current!.put(newParameters[i], bound1);
}
var returnType1 = type1.returnType!.substitute(substitution1);
var returnType2 = type2.returnType!.substitute(substitution2);
var positionalParameters1 =
_substituteList(type1.positionalParameters!, substitution1);
var positionalParameters2 =
_substituteList(type2.positionalParameters!, substitution2);
var namedParameters1 =
_substituteMap(type1.namedParameters!, substitution1);
var namedParameters2 =
_substituteMap(type2.namedParameters!, substitution2);
return RenamedDecoratedFunctionTypes._(
returnType1,
returnType2,
positionalParameters1,
positionalParameters2,
namedParameters1,
namedParameters2);
}
static bool _isNeeded(List<TypeParameterElement>? formals1,
List<TypeParameterElement>? formals2) {
if (identical(formals1, formals2)) return false;
if (formals1!.length != formals2!.length) return true;
for (int i = 0; i < formals1.length; i++) {
if (!identical(formals1[i], formals2[i])) return true;
}
return false;
}
static List<DecoratedType> _substituteList(List<DecoratedType?> list,
Map<TypeParameterElement, DecoratedType> substitution) {
return list.map((t) => t!.substitute(substitution)).toList();
}
static Map<String, DecoratedType> _substituteMap(
Map<String, DecoratedType?> map,
Map<TypeParameterElement, DecoratedType> substitution) {
var result = <String, DecoratedType>{};
for (var entry in map.entries) {
result[entry.key] = entry.value!.substitute(substitution);
}
return result;
}
}