| // 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:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/key.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/path.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/shared.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; | |
| import 'package:_fe_analyzer_shared/src/exhaustiveness/types.dart'; | |
| import 'package:kernel/ast.dart'; | |
| import 'package:kernel/class_hierarchy.dart'; | |
| import 'package:kernel/core_types.dart'; | |
| import 'package:kernel/src/printer.dart'; | |
| import 'package:kernel/src/replacement_visitor.dart'; | |
| import 'package:kernel/type_algebra.dart'; | |
| import 'package:kernel/type_environment.dart'; | |
| import 'constant_evaluator.dart'; | |
| /// AST printer strategy used by default in `CfeTypeOperations.typeToString`. | |
| const AstTextStrategy textStrategy = const AstTextStrategy( | |
| showNullableOnly: true, | |
| useQualifiedTypeParameterNames: false, | |
| ); | |
| /// Data gathered by the exhaustiveness computation, retained for testing | |
| /// purposes. | |
| class ExhaustivenessDataForTesting { | |
| /// Access to interface for looking up `Object` members on non-interface | |
| /// types. | |
| ObjectPropertyLookup? objectFieldLookup; | |
| /// Map from switch statement/expression nodes to the results of the | |
| /// exhaustiveness test. | |
| Map<Node, ExhaustivenessResult> switchResults = {}; | |
| } | |
| // Coverage-ignore(suite): Not run. | |
| class ExhaustivenessResult( | |
| final StaticType scrutineeType, | |
| final List<Space> caseSpaces, | |
| final List<int> caseOffsets, | |
| final Set<int> unreachableCases, | |
| final NonExhaustiveness? nonExhaustiveness, | |
| ); | |
| class CfeTypeOperations( | |
| final TypeEnvironment _typeEnvironment, | |
| final Library _enclosingLibrary, | |
| ) implements TypeOperations<DartType> { | |
| ClassHierarchy get _classHierarchy => _typeEnvironment.hierarchy; | |
| @override | |
| DartType getNonNullable(DartType type) { | |
| return type.toNonNull(); | |
| } | |
| @override | |
| bool isNeverType(DartType type) { | |
| return type is NeverType && type.nullability == Nullability.nonNullable; | |
| } | |
| @override | |
| bool isNonNullableObject(DartType type) { | |
| return type is InterfaceType && | |
| type.classNode == _typeEnvironment.objectClass && | |
| type.nullability == Nullability.nonNullable; | |
| } | |
| @override | |
| bool isNullType(DartType type) { | |
| return type is NullType || | |
| (type is NeverType && | |
| // Coverage-ignore(suite): Not run. | |
| type.nullability == Nullability.nullable); | |
| } | |
| @override | |
| bool isNullable(DartType type) { | |
| return type.declaredNullability == Nullability.nullable; | |
| } | |
| @override | |
| bool isPotentiallyNullable(DartType type) { | |
| return type.nullability != Nullability.nonNullable; | |
| } | |
| @override | |
| bool isNullableObject(DartType type) { | |
| return type == _typeEnvironment.objectNullableRawType; | |
| } | |
| @override | |
| bool isDynamic(DartType type) { | |
| return type is DynamicType; | |
| } | |
| @override | |
| bool isRecordType(DartType type) { | |
| return type is RecordType && !isNullable(type); | |
| } | |
| @override | |
| bool isSubtypeOf(DartType s, DartType t) { | |
| return _typeEnvironment.isSubtypeOf(s, t); | |
| } | |
| @override | |
| DartType get nonNullableObjectType => | |
| _typeEnvironment.objectNonNullableRawType; | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| DartType get nullableObjectType => _typeEnvironment.objectNullableRawType; | |
| @override | |
| DartType get boolType => _typeEnvironment.coreTypes.boolNonNullableRawType; | |
| @override | |
| bool isBoolType(DartType type) { | |
| return type == _typeEnvironment.coreTypes.boolNonNullableRawType; | |
| } | |
| @override | |
| Map<Key, DartType> getFieldTypes(DartType type) { | |
| if (type is InterfaceType) { | |
| Map<Key, DartType> fieldTypes = {}; | |
| Map<Class, Substitution> substitutions = {}; | |
| for (Member member in _classHierarchy.getInterfaceMembers( | |
| type.classNode, | |
| )) { | |
| if (member.name.isPrivate && member.name.library != _enclosingLibrary) { | |
| continue; | |
| } | |
| DartType? fieldType; | |
| if (member is Field) { | |
| fieldType = member.getterType; | |
| } else if (member is Procedure && !member.isSetter) { | |
| fieldType = member.getterType; | |
| } | |
| if (fieldType != null) { | |
| Class declaringClass = member.enclosingClass!; | |
| if (declaringClass.typeParameters.isNotEmpty) { | |
| Substitution substitution = substitutions[declaringClass] ??= | |
| Substitution.fromInterfaceType( | |
| _classHierarchy.getInterfaceTypeAsInstanceOfClass( | |
| type, | |
| declaringClass, | |
| )!, | |
| ); | |
| fieldType = substitution.substituteType(fieldType); | |
| } | |
| fieldTypes[new NameKey(member.name.text)] = fieldType; | |
| } | |
| } | |
| return fieldTypes; | |
| } else if (type is ExtensionType) { | |
| // Coverage-ignore-block(suite): Not run. | |
| ExtensionTypeDeclaration extensionTypeDeclaration = | |
| type.extensionTypeDeclaration; | |
| Map<Key, DartType> fieldTypes = {}; | |
| for (TypeDeclarationType implementedType | |
| in extensionTypeDeclaration.implements) { | |
| Map<Key, DartType> implementedFieldTypes = getFieldTypes( | |
| implementedType, | |
| ); | |
| if (implementedType.typeDeclaration.typeParameters.isNotEmpty) { | |
| Substitution substitution = Substitution.fromTypeDeclarationType( | |
| _classHierarchy.getTypeAsInstanceOf( | |
| type, | |
| implementedType.typeDeclaration, | |
| )!, | |
| ); | |
| for (MapEntry<Key, DartType> entry in implementedFieldTypes.entries) { | |
| fieldTypes[entry.key] = substitution.substituteType(entry.value); | |
| } | |
| } else { | |
| fieldTypes.addAll(implementedFieldTypes); | |
| } | |
| } | |
| Substitution substitution = Substitution.fromExtensionType(type); | |
| for (Procedure procedure in extensionTypeDeclaration.procedures) { | |
| if (!procedure.isSetter) { | |
| DartType fieldType = substitution.substituteType( | |
| procedure.getterType, | |
| ); | |
| fieldTypes[new NameKey(procedure.name.text)] = fieldType; | |
| } | |
| } | |
| for (ExtensionTypeMemberDescriptor descriptor | |
| in extensionTypeDeclaration.memberDescriptors) { | |
| if (descriptor.isStatic) { | |
| continue; | |
| } | |
| switch (descriptor.kind) { | |
| case ExtensionTypeMemberKind.Method: | |
| Procedure tearOff = descriptor.tearOffReference!.asProcedure; | |
| FunctionType functionType = tearOff.getterType as FunctionType; | |
| if (extensionTypeDeclaration.typeParameters.isNotEmpty) { | |
| functionType = FunctionTypeInstantiator.instantiate( | |
| functionType, | |
| type.typeArguments, | |
| ); | |
| } | |
| fieldTypes[new NameKey(descriptor.name.text)] = | |
| functionType.returnType; | |
| case ExtensionTypeMemberKind.Getter: | |
| Procedure member = descriptor.memberReference!.asProcedure; | |
| FunctionType functionType = member.getterType as FunctionType; | |
| if (extensionTypeDeclaration.typeParameters.isNotEmpty) { | |
| functionType = FunctionTypeInstantiator.instantiate( | |
| functionType, | |
| type.typeArguments, | |
| ); | |
| } | |
| fieldTypes[new NameKey(descriptor.name.text)] = | |
| functionType.returnType; | |
| case ExtensionTypeMemberKind.Field: | |
| case ExtensionTypeMemberKind.Constructor: | |
| case ExtensionTypeMemberKind.Factory: | |
| case ExtensionTypeMemberKind.Setter: | |
| case ExtensionTypeMemberKind.Operator: | |
| case ExtensionTypeMemberKind.RedirectingFactory: | |
| break; | |
| } | |
| } | |
| return fieldTypes; | |
| } else if (type is RecordType) { | |
| Map<Key, DartType> fieldTypes = {}; | |
| fieldTypes.addAll( | |
| getFieldTypes(_typeEnvironment.coreTypes.objectNonNullableRawType), | |
| ); | |
| for (int index = 0; index < type.positional.length; index++) { | |
| fieldTypes[new RecordIndexKey(index)] = type.positional[index]; | |
| } | |
| for (NamedType field in type.named) { | |
| fieldTypes[new RecordNameKey(field.name)] = field.type; | |
| } | |
| return fieldTypes; | |
| } else { | |
| return getFieldTypes(_typeEnvironment.coreTypes.objectNonNullableRawType); | |
| } | |
| } | |
| @override | |
| String typeToString(DartType type) => type.toText(textStrategy); | |
| @override | |
| DartType overapproximate(DartType type) { | |
| return TypeParameterReplacer.replaceTypeParameters(type); | |
| } | |
| @override | |
| bool isGeneric(DartType type) { | |
| return type is InterfaceType && type.typeArguments.isNotEmpty; | |
| } | |
| @override | |
| DartType instantiateFuture(DartType type) { | |
| return _typeEnvironment.futureType(type, Nullability.nonNullable); | |
| } | |
| @override | |
| DartType? getFutureOrTypeArgument(DartType type) { | |
| return type is FutureOrType ? type.typeArgument : null; | |
| } | |
| @override | |
| DartType? getListElementType(DartType type) { | |
| type = type.nonTypeParameterBound; | |
| if (type is TypeDeclarationType) { | |
| List<DartType>? typeArguments = _classHierarchy | |
| .getTypeArgumentsAsInstanceOf( | |
| type, | |
| _typeEnvironment.coreTypes.listClass, | |
| ); | |
| if (typeArguments != null) { | |
| return typeArguments[0]; | |
| } | |
| } | |
| return null; | |
| } | |
| @override | |
| DartType? getListType(DartType type) { | |
| type = type.nonTypeParameterBound; | |
| if (type is TypeDeclarationType) { | |
| return _classHierarchy.getTypeAsInstanceOf( | |
| type, | |
| _typeEnvironment.coreTypes.listClass, | |
| ); | |
| } | |
| return null; | |
| } | |
| @override | |
| DartType? getMapValueType(DartType type) { | |
| type = type.nonTypeParameterBound; | |
| if (type is TypeDeclarationType) { | |
| List<DartType>? typeArguments = _classHierarchy | |
| .getTypeArgumentsAsInstanceOf( | |
| type, | |
| _typeEnvironment.coreTypes.mapClass, | |
| ); | |
| if (typeArguments != null) { | |
| return typeArguments[1]; | |
| } | |
| } | |
| return null; | |
| } | |
| @override | |
| bool hasSimpleName(DartType type) { | |
| return type is InterfaceType || | |
| // Coverage-ignore(suite): Not run. | |
| type is DynamicType || | |
| // Coverage-ignore(suite): Not run. | |
| type is VoidType || | |
| // Coverage-ignore(suite): Not run. | |
| type is NeverType || | |
| // Coverage-ignore(suite): Not run. | |
| type is NullType || | |
| // Coverage-ignore(suite): Not run. | |
| type is ExtensionType || | |
| // Coverage-ignore(suite): Not run. | |
| // TODO(johnniwinther): What about intersection types? | |
| type is TypeParameterType; | |
| } | |
| @override | |
| DartType? getTypeVariableBound(DartType type) { | |
| if (type is TypeParameterType) { | |
| return type.bound; | |
| } else if (type is IntersectionType) { | |
| return type.right; | |
| } | |
| return null; | |
| } | |
| @override | |
| DartType getExtensionTypeErasure(DartType type) { | |
| return type.extensionTypeErasure; | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool isEnum(DartType type) { | |
| if (type is InterfaceType) { | |
| return type.classNode.isEnum; | |
| } | |
| return false; | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| Uri? libraryUri(DartType type) { | |
| switch (type) { | |
| case InterfaceType(): | |
| return type.classNode.enclosingLibrary.importUri; | |
| case ExtensionType(): | |
| return type.extensionTypeDeclaration.enclosingLibrary.importUri; | |
| case TypedefType(): | |
| return type.typedefNode.enclosingLibrary.importUri; | |
| case IntersectionType(): | |
| case TypeParameterType(): | |
| case StructuralParameterType(): | |
| case FunctionTypeParameterType(): | |
| case ClassTypeParameterType(): | |
| case AuxiliaryType(): | |
| case InvalidType(): | |
| case DynamicType(): | |
| case VoidType(): | |
| case NeverType(): | |
| case NullType(): | |
| case FunctionType(): | |
| case FutureOrType(): | |
| case RecordType(): | |
| return null; | |
| } | |
| } | |
| } | |
| class EnumValue(final Class enumClass, final String name) { | |
| @override | |
| int get hashCode => Object.hash(enumClass, name); | |
| @override | |
| bool operator ==(other) { | |
| if (identical(this, other)) return true; | |
| return other is EnumValue && | |
| enumClass == other.enumClass && | |
| name == other.name; | |
| } | |
| } | |
| EnumValue? constantToEnumValue(CoreTypes coreTypes, Constant constant) { | |
| if (constant is InstanceConstant && constant.classNode.isEnum) { | |
| StringConstant name = | |
| constant.fieldValues[coreTypes.enumNameField.fieldReference] | |
| as StringConstant; | |
| return new EnumValue(constant.classNode, name.value); | |
| } else if (constant is UnevaluatedConstant) { | |
| Expression expression = constant.expression; | |
| if (expression is FileUriExpression) { | |
| expression = expression.expression; | |
| } | |
| if (expression is InstanceCreation && expression.classNode.isEnum) { | |
| ConstantExpression name = | |
| expression.fieldValues[coreTypes.enumNameField.fieldReference] | |
| as ConstantExpression; | |
| return new EnumValue( | |
| expression.classNode, | |
| (name.constant as StringConstant).value, | |
| ); | |
| } | |
| } | |
| return null; | |
| } | |
| class CfeEnumOperations(final ConstantEvaluator _constantEvaluator) | |
| implements EnumOperations<DartType, Class, Field, EnumValue> { | |
| @override | |
| Class? getEnumClass(DartType type) { | |
| if (type is InterfaceType && type.classNode.isEnum) { | |
| return type.classNode; | |
| } | |
| return null; | |
| } | |
| @override | |
| String getEnumElementName(Field enumField) { | |
| return '${enumField.enclosingClass!.name}.${enumField.name}'; | |
| } | |
| @override | |
| InterfaceType getEnumElementType(Field enumField) { | |
| return enumField.type as InterfaceType; | |
| } | |
| @override | |
| EnumValue? getEnumElementValue(Field enumField) { | |
| // Enum field initializers might not have been replaced by | |
| // [ConstantExpression]s. Either because we haven't visited them yet during | |
| // normal constant evaluation or because they are from outlines that are | |
| // not part of the fully compiled libraries. Therefore we perform constant | |
| // evaluation here, to ensure that we have the [Constant] value for the | |
| // enum element. | |
| StaticTypeContext context = new StaticTypeContext( | |
| enumField, | |
| _constantEvaluator.typeEnvironment, | |
| ); | |
| return constantToEnumValue( | |
| _constantEvaluator.coreTypes, | |
| _constantEvaluator.evaluate(context, enumField.initializer!), | |
| ); | |
| } | |
| @override | |
| Iterable<Field> getEnumElements(Class enumClass) sync* { | |
| for (Field field in enumClass.fields) { | |
| if (field.isEnumElement) { | |
| yield field; | |
| } | |
| } | |
| } | |
| } | |
| class CfeSealedClassOperations(final TypeEnvironment _typeEnvironment) | |
| implements SealedClassOperations<DartType, Class> { | |
| @override | |
| List<Class> getDirectSubclasses(Class sealedClass) { | |
| Library library = sealedClass.enclosingLibrary; | |
| List<Class> list = []; | |
| outer: | |
| for (Class cls in library.classes) { | |
| if (cls == sealedClass) continue; | |
| Class? superclass = cls.superclass; | |
| while (superclass != null) { | |
| if (!superclass.isMixinApplication) { | |
| if (superclass == sealedClass) { | |
| list.add(cls); | |
| continue outer; | |
| } | |
| break; | |
| } else { | |
| // Mixed in class are encoded through unnamed mixin applications: | |
| // | |
| // class Class extends Super with Mixin {} | |
| // | |
| // => | |
| // | |
| // class _Super&Mixin extends Super /* mixedInClass = Mixin */ | |
| // class Class extends _Super&Mixin {} | |
| // | |
| if (superclass.mixedInClass == sealedClass) { | |
| // Coverage-ignore-block(suite): Not run. | |
| list.add(cls); | |
| continue outer; | |
| } | |
| superclass = superclass.superclass; | |
| } | |
| } | |
| for (Supertype interface in cls.implementedTypes) { | |
| if (interface.classNode == sealedClass) { | |
| list.add(cls); | |
| continue outer; | |
| } | |
| } | |
| } | |
| return list; | |
| } | |
| @override | |
| Class? getSealedClass(DartType type) { | |
| if (type is InterfaceType && type.classNode.isSealed) { | |
| return type.classNode; | |
| } | |
| return null; | |
| } | |
| @override | |
| DartType? getSubclassAsInstanceOf( | |
| Class subClass, | |
| covariant InterfaceType sealedClassType, | |
| ) { | |
| InterfaceType thisType = subClass.getThisType( | |
| _typeEnvironment.coreTypes, | |
| Nullability.nonNullable, | |
| ); | |
| InterfaceType asSealedType = _typeEnvironment.hierarchy | |
| .getInterfaceTypeAsInstanceOfClass( | |
| thisType, | |
| sealedClassType.classNode, | |
| )!; | |
| if (thisType.typeArguments.isEmpty) { | |
| return thisType; | |
| } | |
| // Coverage-ignore-block(suite): Not run. | |
| bool trivialSubstitution = true; | |
| if (thisType.typeArguments.length == asSealedType.typeArguments.length) { | |
| for (int i = 0; i < thisType.typeArguments.length; i++) { | |
| if (thisType.typeArguments[i] != asSealedType.typeArguments[i]) { | |
| trivialSubstitution = false; | |
| break; | |
| } | |
| } | |
| if (trivialSubstitution) { | |
| Substitution substitution = Substitution.fromPairs( | |
| subClass.typeParameters, | |
| sealedClassType.typeArguments, | |
| ); | |
| for (int i = 0; i < subClass.typeParameters.length; i++) { | |
| DartType bound = substitution.substituteType( | |
| subClass.typeParameters[i].bound, | |
| ); | |
| if (!_typeEnvironment.isSubtypeOf( | |
| sealedClassType.typeArguments[i], | |
| bound, | |
| )) { | |
| trivialSubstitution = false; | |
| break; | |
| } | |
| } | |
| } | |
| } else { | |
| trivialSubstitution = false; | |
| } | |
| if (trivialSubstitution) { | |
| return new InterfaceType( | |
| subClass, | |
| Nullability.nonNullable, | |
| sealedClassType.typeArguments, | |
| ); | |
| } else { | |
| return TypeParameterReplacer.replaceTypeParameters(thisType); | |
| } | |
| } | |
| } | |
| class CfeExhaustivenessCache | |
| extends ExhaustivenessCache<DartType, Class, Class, Field, EnumValue> { | |
| final TypeEnvironment typeEnvironment; | |
| new(ConstantEvaluator constantEvaluator, Library enclosingLibrary) | |
| : typeEnvironment = constantEvaluator.typeEnvironment, | |
| super( | |
| new CfeTypeOperations( | |
| constantEvaluator.typeEnvironment, | |
| enclosingLibrary, | |
| ), | |
| new CfeEnumOperations(constantEvaluator), | |
| new CfeSealedClassOperations(constantEvaluator.typeEnvironment), | |
| ); | |
| } | |
| class PatternConverter with SpaceCreator<Pattern, DartType> { | |
| final Version languageVersion; | |
| final CfeExhaustivenessCache cache; | |
| final StaticTypeContext context; | |
| final bool Function(Constant) hasPrimitiveEquality; | |
| new( | |
| this.languageVersion, | |
| this.cache, | |
| this.context, { | |
| required this.hasPrimitiveEquality, | |
| }); | |
| @override | |
| bool hasLanguageVersion(int major, int minor) { | |
| return languageVersion >= new Version(major, minor); | |
| } | |
| @override | |
| Space dispatchPattern( | |
| Path path, | |
| StaticType contextType, | |
| Pattern pattern, { | |
| required bool nonNull, | |
| }) { | |
| if (pattern is ObjectPattern) { | |
| Map<String, Pattern> properties = {}; | |
| Map<String, DartType> extensionPropertyTypes = {}; | |
| for (NamedPattern field in pattern.fields) { | |
| properties[field.name] = field.pattern; | |
| switch (field.accessKind) { | |
| case ObjectAccessKind.Extension: | |
| case ObjectAccessKind.ExtensionType: | |
| case ObjectAccessKind.Direct: | |
| extensionPropertyTypes[field.name] = field.resultType!; | |
| case ObjectAccessKind.Object: | |
| case ObjectAccessKind.Instance: | |
| case ObjectAccessKind.RecordNamed: | |
| case ObjectAccessKind.RecordIndexed: | |
| case ObjectAccessKind.Dynamic: | |
| case ObjectAccessKind.Never: | |
| case ObjectAccessKind.Invalid: | |
| case ObjectAccessKind.FunctionTearOff: | |
| case ObjectAccessKind.Error: | |
| } | |
| } | |
| return createObjectSpace( | |
| path, | |
| contextType, | |
| pattern.requiredType, | |
| properties, | |
| extensionPropertyTypes, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is VariablePattern) { | |
| return createVariableSpace( | |
| path, | |
| contextType, | |
| pattern.variable.type, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is ConstantPattern) { | |
| return convertConstantToSpace(pattern.value!, path: path); | |
| } else if (pattern is RecordPattern) { | |
| List<Pattern> positional = []; | |
| Map<String, Pattern> named = {}; | |
| for (Pattern field in pattern.patterns) { | |
| if (field is NamedPattern) { | |
| named[field.name] = field.pattern; | |
| } else { | |
| positional.add(field); | |
| } | |
| } | |
| return createRecordSpace( | |
| path, | |
| contextType, | |
| pattern.requiredType, | |
| positional, | |
| named, | |
| ); | |
| } else if (pattern is WildcardPattern) { | |
| return createWildcardSpace( | |
| path, | |
| contextType, | |
| pattern.type, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is OrPattern) { | |
| return createLogicalOrSpace( | |
| path, | |
| contextType, | |
| pattern.left, | |
| pattern.right, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is NullCheckPattern) { | |
| return createNullCheckSpace(path, contextType, pattern.pattern); | |
| } else if (pattern is NullAssertPattern) { | |
| return createNullAssertSpace(path, contextType, pattern.pattern); | |
| } else if (pattern is CastPattern) { | |
| return createCastSpace( | |
| path, | |
| contextType, | |
| pattern.type, | |
| pattern.pattern, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is AndPattern) { | |
| return createLogicalAndSpace( | |
| path, | |
| contextType, | |
| pattern.left, | |
| pattern.right, | |
| nonNull: nonNull, | |
| ); | |
| } else if (pattern is InvalidPattern) { | |
| // These pattern do not add to the exhaustiveness coverage. | |
| return createUnknownSpace(path); | |
| } else if (pattern is RelationalPattern) { | |
| return createRelationalSpace(path); | |
| } else if (pattern is ListPattern) { | |
| InterfaceType type = pattern.requiredType as InterfaceType; | |
| assert( | |
| type.classNode == cache.typeEnvironment.coreTypes.listClass && | |
| type.typeArguments.length == 1, | |
| ); | |
| DartType elementType = type.typeArguments[0]; | |
| bool hasRest = false; | |
| List<Pattern> headPatterns = []; | |
| Pattern? restPattern; | |
| List<Pattern> tailPatterns = []; | |
| for (Pattern element in pattern.patterns) { | |
| if (element is RestPattern) { | |
| hasRest = true; | |
| restPattern = element.subPattern; | |
| } else if (hasRest) { | |
| tailPatterns.add(element); | |
| } else { | |
| headPatterns.add(element); | |
| } | |
| } | |
| return createListSpace( | |
| path, | |
| type: pattern.requiredType, | |
| elementType: elementType, | |
| headElements: headPatterns, | |
| restElement: restPattern, | |
| tailElements: tailPatterns, | |
| hasRest: hasRest, | |
| hasExplicitTypeArgument: pattern.typeArgument != null, | |
| ); | |
| } else if (pattern is MapPattern) { | |
| InterfaceType type = pattern.requiredType as InterfaceType; | |
| assert( | |
| type.classNode == cache.typeEnvironment.coreTypes.mapClass && | |
| type.typeArguments.length == 2, | |
| ); | |
| DartType keyType = type.typeArguments[0]; | |
| DartType valueType = type.typeArguments[1]; | |
| Map<MapKey, Pattern> entries = {}; | |
| for (MapPatternEntry entry in pattern.entries) { | |
| if (entry is MapPatternRestEntry) { | |
| // Rest patterns are illegal in map patterns, so just skip over it. | |
| } else { | |
| Constant constant = entry.keyValue!; | |
| MapKey key = new MapKey(constant, constant.toText(textStrategy)); | |
| entries[key] = entry.value; | |
| } | |
| } | |
| return createMapSpace( | |
| path, | |
| type: pattern.lookupType, | |
| keyType: keyType, | |
| valueType: valueType, | |
| entries: entries, | |
| hasExplicitTypeArguments: | |
| pattern.keyType != null && pattern.valueType != null, | |
| ); | |
| } | |
| // Coverage-ignore-block(suite): Not run. | |
| assert(false, "Unexpected pattern $pattern (${pattern.runtimeType})."); | |
| return createUnknownSpace(path); | |
| } | |
| Space convertConstantToSpace(Constant constant, {required Path path}) { | |
| EnumValue? enumValue = constantToEnumValue( | |
| cache.typeEnvironment.coreTypes, | |
| constant, | |
| ); | |
| if (enumValue != null) { | |
| return new Space( | |
| path, | |
| cache.getEnumElementStaticType(enumValue.enumClass, enumValue), | |
| ); | |
| } else if (constant is NullConstant) { | |
| return new Space(path, StaticType.nullType); | |
| } else if (constant is BoolConstant) { | |
| return new Space(path, cache.getBoolValueStaticType(constant.value)); | |
| } else if (constant is RecordConstant) { | |
| Map<Key, Space> properties = {}; | |
| for ( | |
| int index = 0; | |
| index < constant.positional.length; | |
| // Coverage-ignore(suite): Not run. | |
| index++ | |
| ) { | |
| // Coverage-ignore-block(suite): Not run. | |
| Key key = new RecordIndexKey(index); | |
| properties[key] = convertConstantToSpace( | |
| constant.positional[index], | |
| path: path.add(key), | |
| ); | |
| } | |
| for (MapEntry<String, Constant> entry in constant.named.entries) { | |
| // Coverage-ignore-block(suite): Not run. | |
| Key key = new RecordNameKey(entry.key); | |
| properties[key] = convertConstantToSpace( | |
| entry.value, | |
| path: path.add(key), | |
| ); | |
| } | |
| return new Space( | |
| path, | |
| cache.getStaticType(constant.recordType), | |
| properties: properties, | |
| ); | |
| } else if (hasPrimitiveEquality(constant)) { | |
| // Only if [constant] has primitive equality can we tell if it is equal | |
| // to itself. | |
| return new Space( | |
| path, | |
| cache.getUniqueStaticType<Constant>( | |
| constant.getType(context), | |
| constant, | |
| constant.toText(textStrategy), | |
| ), | |
| ); | |
| } else { | |
| return new Space(path, cache.getUnknownStaticType()); | |
| } | |
| } | |
| @override | |
| StaticType createUnknownStaticType() { | |
| return cache.getUnknownStaticType(); | |
| } | |
| @override | |
| StaticType createStaticType(DartType type) { | |
| return cache.getStaticType(type); | |
| } | |
| @override | |
| StaticType createListType( | |
| DartType type, | |
| ListTypeRestriction<DartType> restriction, | |
| ) { | |
| return cache.getListStaticType(type, restriction); | |
| } | |
| @override | |
| StaticType createMapType( | |
| DartType type, | |
| MapTypeRestriction<DartType> restriction, | |
| ) { | |
| return cache.getMapStaticType(type, restriction); | |
| } | |
| @override | |
| TypeOperations<DartType> get typeOperations => cache.typeOperations; | |
| @override | |
| ObjectPropertyLookup get objectFieldLookup => cache; | |
| } | |
| bool computeIsAlwaysExhaustiveType(DartType type, CoreTypes coreTypes) { | |
| return type.accept1(const ExhaustiveDartTypeVisitor(), coreTypes); | |
| } | |
| class const ExhaustiveDartTypeVisitor() | |
| with DartTypeVisitor1ExperimentExclusionMixin<bool, CoreTypes> | |
| implements DartTypeVisitor1<bool, CoreTypes> { | |
| @override | |
| bool visitAuxiliaryType(AuxiliaryType node, CoreTypes coreTypes) { | |
| throw new UnsupportedError( | |
| "Unsupported auxiliary type $node (${node.runtimeType}).", | |
| ); | |
| } | |
| @override | |
| bool visitDynamicType(DynamicType type, CoreTypes coreTypes) { | |
| return false; | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool visitFunctionType(FunctionType type, CoreTypes coreTypes) { | |
| return false; | |
| } | |
| @override | |
| bool visitFutureOrType(FutureOrType type, CoreTypes coreTypes) { | |
| return type.typeArgument.accept1(this, coreTypes); | |
| } | |
| @override | |
| bool visitExtensionType(ExtensionType type, CoreTypes coreTypes) { | |
| return type.extensionTypeErasure.accept1(this, coreTypes); | |
| } | |
| @override | |
| bool visitInterfaceType(InterfaceType type, CoreTypes coreTypes) { | |
| if (type.classNode == coreTypes.boolClass) { | |
| return true; | |
| } else if (type.classNode.isEnum) { | |
| return true; | |
| } else if (type.classNode.isSealed) { | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| @override | |
| bool visitIntersectionType(IntersectionType type, CoreTypes coreTypes) { | |
| return type.right.accept1(this, coreTypes); | |
| } | |
| @override | |
| bool visitInvalidType(InvalidType type, CoreTypes coreTypes) { | |
| return false; | |
| } | |
| @override | |
| bool visitNeverType(NeverType type, CoreTypes coreTypes) { | |
| return false; | |
| } | |
| @override | |
| bool visitNullType(NullType type, CoreTypes coreTypes) { | |
| return true; | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool visitRecordType(RecordType type, CoreTypes coreTypes) { | |
| for (DartType positional in type.positional) { | |
| if (!positional.accept1(this, coreTypes)) { | |
| return false; | |
| } | |
| } | |
| for (NamedType named in type.named) { | |
| if (!named.type.accept1(this, coreTypes)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| @override | |
| bool visitTypeParameterType(TypeParameterType type, CoreTypes coreTypes) { | |
| return type.bound.accept1(this, coreTypes); | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool visitStructuralParameterType( | |
| StructuralParameterType type, | |
| CoreTypes coreTypes, | |
| ) { | |
| return type.bound.accept1(this, coreTypes); | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool visitTypedefType(TypedefType type, CoreTypes coreTypes) { | |
| return type.unalias.accept1(this, coreTypes); | |
| } | |
| @override | |
| // Coverage-ignore(suite): Not run. | |
| bool visitVoidType(VoidType type, CoreTypes coreTypes) { | |
| return false; | |
| } | |
| } | |
| class const TypeParameterReplacer() extends ReplacementVisitor { | |
| @override | |
| DartType? visitTypeParameterType(TypeParameterType node, Variance variance) { | |
| DartType replacement = super.visitTypeParameterType(node, variance) ?? node; | |
| if (replacement is TypeParameterType) { | |
| if (variance == Variance.contravariant) { | |
| // Coverage-ignore-block(suite): Not run. | |
| return _replaceTypeParameterTypes( | |
| const NeverType.nonNullable(), | |
| variance, | |
| ); | |
| } else { | |
| return _replaceTypeParameterTypes( | |
| replacement.parameter.defaultType, | |
| variance, | |
| ); | |
| } | |
| } | |
| return replacement; | |
| } | |
| DartType _replaceTypeParameterTypes(DartType type, Variance variance) { | |
| return type.accept1(this, variance) ?? type; | |
| } | |
| static DartType replaceTypeParameters(DartType type) { | |
| return const TypeParameterReplacer()._replaceTypeParameterTypes( | |
| type, | |
| Variance.covariant, | |
| ); | |
| } | |
| } |