// 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. /// @docImport 'package:analyzer/src/error/codes.dart'; library; import 'package:_fe_analyzer_shared/src/exhaustiveness/dart_template_buffer.dart'; 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:_fe_analyzer_shared/src/types/shared_type.dart' show Variance; import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/src/dart/ast/ast.dart'; import 'package:analyzer/src/dart/ast/extensions.dart'; import 'package:analyzer/src/dart/constant/value.dart'; import 'package:analyzer/src/dart/element/element.dart'; import 'package:analyzer/src/dart/element/replacement_visitor.dart'; import 'package:analyzer/src/dart/element/type.dart'; import 'package:analyzer/src/dart/element/type_algebra.dart'; import 'package:analyzer/src/dart/element/type_system.dart'; import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; import 'package:pub_semver/pub_semver.dart'; /// The buffer that accumulates types and elements as is, so that they /// can be written latter into Dart code that considers imports. It also /// accumulates fragments of text, such as syntax `(`, or names of properties. class AnalyzerDartTemplateBuffer implements DartTemplateBuffer { final List parts = []; bool isComplete = true; @override void write(String text) { parts.add(MissingPatternTextPart(text)); } @override void writeBoolValue(bool value) { parts.add(MissingPatternTextPart('$value')); } @override void writeCoreType(String name) { parts.add(MissingPatternTextPart(name)); } @override void writeEnumValue(FieldElement value, String name) { var enumElement = value.enclosingElement; if (enumElement is! EnumElement) { isComplete = false; return; } parts.add( MissingPatternEnumValuePart(enumElement2: enumElement, value2: value), ); } @override void writeGeneralConstantValue(DartObject value, String name) { isComplete = false; } @override void writeGeneralType(TypeImpl type, String name) { parts.add(MissingPatternTypePart(type)); } } class AnalyzerEnumOperations implements EnumOperations { const AnalyzerEnumOperations(); @override EnumElement? getEnumClass(TypeImpl type) { var element = type.element; if (element is EnumElement) { return element; } return null; } @override String getEnumElementName(FieldElement enumField) { return '${enumField.enclosingElement.name}.${enumField.name}'; } @override Iterable getEnumElements(EnumElement enumClass) sync* { for (var field in enumClass.fields) { if (field.isEnumConstant) { yield field; } } } @override InterfaceTypeImpl getEnumElementType(FieldElement enumField) { return enumField.type as InterfaceTypeImpl; } @override DartObject? getEnumElementValue(FieldElement enumField) { return enumField.computeConstantValue(); } } class AnalyzerExhaustivenessCache extends ExhaustivenessCache< TypeImpl, InterfaceElement, EnumElement, FieldElement, DartObject > { final TypeSystemImpl typeSystem; AnalyzerExhaustivenessCache(this.typeSystem, LibraryElement enclosingLibrary) : super( AnalyzerTypeOperations(typeSystem, enclosingLibrary), const AnalyzerEnumOperations(), AnalyzerSealedClassOperations(typeSystem), ); } class AnalyzerSealedClassOperations implements SealedClassOperations { final TypeSystemImpl _typeSystem; AnalyzerSealedClassOperations(this._typeSystem); @override List getDirectSubclasses( InterfaceElementImpl sealedClass, ) { // Only classes can be sealed. sealedClass as ClassElementImpl; return sealedClass.directSubtypesOfSealed; } @override ClassElementImpl? getSealedClass(TypeImpl type) { var element = type.element; if (element is ClassElementImpl && element.isSealed) { return element; } return null; } @override TypeImpl? getSubclassAsInstanceOf( InterfaceElementImpl subClass, covariant InterfaceTypeImpl sealedClassType, ) { var thisType = subClass.thisType; var asSealedClass = thisType.asInstanceOf(sealedClassType.element)!; if (thisType.typeArguments.isEmpty) { return thisType; } bool trivialSubstitution = true; if (thisType.typeArguments.length == asSealedClass.typeArguments.length) { for (int i = 0; i < thisType.typeArguments.length; i++) { if (thisType.typeArguments[i] != asSealedClass.typeArguments[i]) { trivialSubstitution = false; break; } } if (trivialSubstitution) { Substitution substitution = Substitution.fromPairs2( subClass.typeParameters, sealedClassType.typeArguments, ); for (int i = 0; i < subClass.typeParameters.length; i++) { var bound = subClass.typeParameters[i].bound; if (bound != null && !_typeSystem.isSubtypeOf( sealedClassType.typeArguments[i], substitution.substituteType(bound), )) { trivialSubstitution = false; break; } } } } else { trivialSubstitution = false; } if (trivialSubstitution) { return subClass.instantiateImpl( typeArguments: sealedClassType.typeArguments, nullabilitySuffix: NullabilitySuffix.none, ); } else { return TypeParameterReplacer.replaceTypeVariables(_typeSystem, thisType); } } } class AnalyzerTypeOperations implements TypeOperations { final TypeSystemImpl _typeSystem; final LibraryElement _enclosingLibrary; final Map> _interfaceFieldTypesCaches = {}; AnalyzerTypeOperations(this._typeSystem, this._enclosingLibrary); @override TypeImpl get boolType => _typeSystem.typeProvider.boolType; @override TypeImpl get nonNullableObjectType => _typeSystem.objectNone; @override TypeImpl get nullableObjectType => _typeSystem.objectQuestion; @override TypeImpl getExtensionTypeErasure(TypeImpl type) { return type.extensionTypeErasure; } @override Map getFieldTypes(TypeImpl type) { if (type is InterfaceTypeImpl) { return _getInterfaceFieldTypes(type); } else if (type is RecordTypeImpl) { Map fieldTypes = {}; fieldTypes.addAll(getFieldTypes(_typeSystem.typeProvider.objectType)); for (int index = 0; index < type.positionalFields.length; index++) { var field = type.positionalFields[index]; fieldTypes[RecordIndexKey(index)] = field.type; } for (var field in type.namedFields) { fieldTypes[RecordNameKey(field.name)] = field.type; } return fieldTypes; } return getFieldTypes(_typeSystem.typeProvider.objectType); } @override TypeImpl? getFutureOrTypeArgument(TypeImpl type) { return type.isDartAsyncFutureOr ? _typeSystem.futureOrBase(type) : null; } @override TypeImpl? getListElementType(TypeImpl type) { var listType = type.asInstanceOf(_typeSystem.typeProvider.listElement); if (listType != null) { return listType.typeArguments[0]; } return null; } @override TypeImpl? getListType(TypeImpl type) { return type.asInstanceOf(_typeSystem.typeProvider.listElement); } @override TypeImpl? getMapValueType(TypeImpl type) { var mapType = type.asInstanceOf(_typeSystem.typeProvider.mapElement); if (mapType != null) { return mapType.typeArguments[1]; } return null; } @override TypeImpl getNonNullable(TypeImpl type) { return _typeSystem.promoteToNonNull(type); } @override TypeImpl? getTypeVariableBound(TypeImpl type) { if (type is TypeParameterTypeImpl) { return type.bound; } return null; } @override bool hasSimpleName(TypeImpl type) { return type is InterfaceTypeImpl || type is DynamicTypeImpl || type is VoidTypeImpl || type is NeverTypeImpl || // TODO(johnniwinther): What about intersection types? type is TypeParameterTypeImpl; } @override TypeImpl instantiateFuture(TypeImpl type) { return _typeSystem.typeProvider.futureType(type); } @override bool isBoolType(TypeImpl type) { return type.isDartCoreBool && !isNullable(type); } @override bool isDynamic(TypeImpl type) { return type is DynamicTypeImpl; } @override bool isEnum(TypeImpl type) { if (type is InterfaceTypeImpl) { return type.element is EnumElement; } return false; } @override bool isGeneric(TypeImpl type) { return type is InterfaceTypeImpl && type.typeArguments.isNotEmpty; } @override bool isNeverType(TypeImpl type) { return type is NeverTypeImpl; } @override bool isNonNullableObject(TypeImpl type) { return type.isDartCoreObject && !isNullable(type); } @override bool isNullable(TypeImpl type) { return type.nullabilitySuffix == NullabilitySuffix.question; } @override bool isNullableObject(TypeImpl type) { return type.isDartCoreObject && isNullable(type); } @override bool isNullType(TypeImpl type) { return type.isDartCoreNull; } @override bool isPotentiallyNullable(TypeImpl type) => _typeSystem.isPotentiallyNullable(type); @override bool isRecordType(TypeImpl type) { return type is RecordTypeImpl && !isNullable(type); } @override bool isSubtypeOf(TypeImpl s, TypeImpl t) { return _typeSystem.isSubtypeOf(s, t); } @override Uri? libraryUri(TypeImpl type) { return type.element?.library?.uri; } @override TypeImpl overapproximate(TypeImpl type) { return TypeParameterReplacer.replaceTypeVariables(_typeSystem, type); } @override String typeToString(TypeImpl type) => type.toString(); Map _getInterfaceFieldTypes(InterfaceTypeImpl type) { var fieldTypes = _interfaceFieldTypesCaches[type]; if (fieldTypes == null) { _interfaceFieldTypesCaches[type] = fieldTypes = {}; for (var supertype in type.allSupertypes) { fieldTypes.addAll(_getInterfaceFieldTypes(supertype)); } for (var getter in type.getters) { if (getter.isPrivate && getter.library != _enclosingLibrary) { continue; } var name = getter.name; if (name == null) { continue; } if (!getter.isStatic) { fieldTypes[NameKey(name)] = getter.type.returnType; } } for (var method in type.methods) { if (method.isPrivate && method.library != _enclosingLibrary) { continue; } var name = method.name; if (name == null) { continue; } if (!method.isStatic) { fieldTypes[NameKey(name)] = method.type; } } } return fieldTypes; } } /// Data gathered by the exhaustiveness computation, retained for testing /// purposes. class ExhaustivenessDataForTesting { /// Access to interface for looking up `Object` members on non-interface /// types. final ObjectPropertyLookup objectFieldLookup; /// Map from switch statement/expression nodes to the static type of the /// scrutinee. Map switchScrutineeType = {}; /// Map from switch statement/expression nodes the spaces for its cases. Map> switchCases = {}; /// Map from switch case nodes to the space for its pattern/expression. Map caseSpaces = {}; /// Map from unreachable switch case nodes to information about their /// unreachability. Map caseUnreachabilities = {}; /// Map from switch statement nodes that are erroneous due to being /// non-exhaustive, to information about their non-exhaustiveness. Map nonExhaustivenesses = {}; ExhaustivenessDataForTesting(this.objectFieldLookup); } class MissingPatternEnumValuePart extends MissingPatternPart { final EnumElement enumElement2; final FieldElement value2; MissingPatternEnumValuePart({ required this.enumElement2, required this.value2, }); @override String toString() => value2.name!; } abstract class MissingPatternPart { /// Expando associating each /// [diag.nonExhaustiveSwitchExpression] or /// [diag.nonExhaustiveSwitchStatement] diagnostic with a /// list of missing patterns; this data is used by the analysis server to add /// missing switch cases. static final byDiagnostic = Expando>>(); } class MissingPatternTextPart extends MissingPatternPart { final String text; MissingPatternTextPart(this.text); @override String toString() => text; } class MissingPatternTypePart extends MissingPatternPart { final TypeImpl type; MissingPatternTypePart(this.type); @override String toString() { return type.getDisplayString(); } } class PatternConverter with SpaceCreator { final Version languageVersion; final FeatureSet featureSet; final AnalyzerExhaustivenessCache cache; final Map mapPatternKeyValues; final Map constantPatternValues; /// If we saw an invalid type, we already have a diagnostic reported, /// and there is no need to verify exhaustiveness. bool hasInvalidType = false; PatternConverter({ required this.languageVersion, required this.featureSet, required this.cache, required this.mapPatternKeyValues, required this.constantPatternValues, }); @override ObjectPropertyLookup get objectFieldLookup => cache; @override TypeOperations get typeOperations => cache.typeOperations; @override StaticType createListType( TypeImpl type, ListTypeRestriction restriction, ) { return cache.getListStaticType(type, restriction); } @override StaticType createMapType( TypeImpl type, MapTypeRestriction restriction, ) { return cache.getMapStaticType(type, restriction); } @override StaticType createStaticType(TypeImpl type) { hasInvalidType |= type is InvalidTypeImpl; return cache.getStaticType(type); } @override StaticType createUnknownStaticType() { return cache.getUnknownStaticType(); } @override Space dispatchPattern( Path path, StaticType contextType, DartPattern pattern, { required bool nonNull, }) { if (pattern is DeclaredVariablePatternImpl) { return createVariableSpace( path, contextType, pattern.declaredFragment!.element.type, nonNull: nonNull, ); } else if (pattern is ObjectPattern) { var properties = {}; var extensionPropertyTypes = {}; for (var field in pattern.fields) { var name = field.effectiveName; if (name == null) { // Error case, skip field. continue; } properties[name] = field.pattern; var element = field.element; TypeImpl? extensionPropertyType; if (element is InternalPropertyAccessorElement && (element.enclosingElement is ExtensionElementImpl || element.enclosingElement is ExtensionTypeElementImpl)) { extensionPropertyType = element.returnType; } else if (element is InternalExecutableElement && (element.enclosingElement is ExtensionElementImpl || element.enclosingElement is ExtensionTypeElementImpl)) { extensionPropertyType = element.type; } if (extensionPropertyType != null) { extensionPropertyTypes[name] = extensionPropertyType; } } return createObjectSpace( path, contextType, pattern.type.typeOrThrow, properties, extensionPropertyTypes, nonNull: nonNull, ); } else if (pattern is WildcardPattern) { return createWildcardSpace( path, contextType, pattern.type?.typeOrThrow, nonNull: nonNull, ); } else if (pattern is RecordPatternImpl) { var positionalTypes = []; var positionalPatterns = []; var namedTypes = {}; var namedPatterns = {}; for (var field in pattern.fields) { var nameNode = field.name; if (nameNode == null) { positionalTypes.add(cache.typeSystem.typeProvider.dynamicType); positionalPatterns.add(field.pattern); } else { String? name = field.effectiveName; if (name != null) { namedTypes[name] = cache.typeSystem.typeProvider.dynamicType; namedPatterns[name] = field.pattern; } else { // Error case, skip field. continue; } } } var recordType = RecordTypeImpl.fromApi( positional: positionalTypes, named: namedTypes, nullabilitySuffix: NullabilitySuffix.none, ); return createRecordSpace( path, contextType, recordType, positionalPatterns, namedPatterns, ); } else if (pattern is LogicalOrPattern) { return createLogicalOrSpace( path, contextType, pattern.leftOperand, pattern.rightOperand, nonNull: nonNull, ); } else if (pattern is NullCheckPattern) { return createNullCheckSpace(path, contextType, pattern.pattern); } else if (pattern is ParenthesizedPattern) { return dispatchPattern( path, contextType, pattern.pattern, nonNull: nonNull, ); } else if (pattern is NullAssertPattern) { return createNullAssertSpace(path, contextType, pattern.pattern); } else if (pattern is CastPattern) { return createCastSpace( path, contextType, pattern.type.typeOrThrow, pattern.pattern, nonNull: nonNull, ); } else if (pattern is LogicalAndPattern) { return createLogicalAndSpace( path, contextType, pattern.leftOperand, pattern.rightOperand, nonNull: nonNull, ); } else if (pattern is RelationalPattern) { return createRelationalSpace(path); } else if (pattern is ListPattern) { var type = pattern.requiredType as InterfaceTypeImpl; assert( type.element == cache.typeSystem.typeProvider.listElement && type.typeArguments.length == 1, ); var elementType = type.typeArguments[0]; List headElements = []; DartPattern? restElement; List tailElements = []; bool hasRest = false; for (ListPatternElement element in pattern.elements) { if (element is RestPatternElement) { restElement = element.pattern; hasRest = true; } else if (hasRest) { tailElements.add(element as DartPattern); } else { headElements.add(element as DartPattern); } } return createListSpace( path, type: type, elementType: elementType, headElements: headElements, tailElements: tailElements, restElement: restElement, hasRest: hasRest, hasExplicitTypeArgument: pattern.typeArguments != null, ); } else if (pattern is MapPattern) { var type = pattern.requiredType as InterfaceTypeImpl; assert( type.element == cache.typeSystem.typeProvider.mapElement && type.typeArguments.length == 2, ); var keyType = type.typeArguments[0]; var valueType = type.typeArguments[1]; Map entries = {}; for (MapPatternElement entry in pattern.elements) { if (entry is RestPatternElement) { // Rest patterns are illegal in map patterns, so just skip over it. } else { Expression expression = (entry as MapPatternEntry).key; // TODO(johnniwinther): Assert that we have a constant value. DartObjectImpl? constant = mapPatternKeyValues[expression]; if (constant == null) { return createUnknownSpace(path); } MapKey key = MapKey(constant, constant.state.toString()); entries[key] = entry.value; } } return createMapSpace( path, type: cache.typeSystem.typeProvider.mapType(keyType, valueType), keyType: keyType, valueType: valueType, entries: entries, hasExplicitTypeArguments: pattern.typeArguments != null, ); } else if (pattern is ConstantPattern) { var value = constantPatternValues[pattern]; if (value != null) { return _convertConstantValue(value, path); } hasInvalidType = true; return createUnknownSpace(path); } assert(false, "Unexpected pattern $pattern (${pattern.runtimeType})"); return createUnknownSpace(path); } @override bool hasLanguageVersion(int major, int minor) { return languageVersion >= Version(major, minor, 0); } Space _convertConstantValue(DartObjectImpl value, Path path) { var type = value.type; var state = value.state; if (value.isNull) { return Space(path, StaticType.nullType); } else if (state is BoolState) { var value = state.value; if (value != null) { return Space(path, cache.getBoolValueStaticType(state.value!)); } } else if (state is RecordState) { var properties = {}; for (var index = 0; index < state.positionalFields.length; index++) { var key = RecordIndexKey(index); var value = state.positionalFields[index]; properties[key] = _convertConstantValue(value, path.add(key)); } for (var entry in state.namedFields.entries) { var key = RecordNameKey(entry.key); properties[key] = _convertConstantValue(entry.value, path.add(key)); } return Space(path, cache.getStaticType(type), properties: properties); } if (type is InterfaceTypeImpl) { var element = type.element; if (element is EnumElementImpl) { return Space(path, cache.getEnumElementStaticType(element, value)); } } StaticType staticType; if (value.hasPrimitiveEquality(featureSet)) { staticType = cache.getUniqueStaticType( type, value, value.state.toString(), ); } else { // If [value] doesn't have primitive equality we cannot tell if it is // equal to itself. staticType = cache.getUnknownStaticType(); } return Space(path, staticType); } } class TypeParameterReplacer extends ReplacementVisitor { final TypeSystemImpl _typeSystem; Variance _variance = Variance.covariant; TypeParameterReplacer(this._typeSystem); @override void changeVariance() { if (_variance == Variance.covariant) { _variance = Variance.contravariant; } else if (_variance == Variance.contravariant) { _variance = Variance.covariant; } } @override TypeImpl? visitTypeParameterBound(covariant TypeImpl type) { Variance savedVariance = _variance; _variance = Variance.invariant; var result = type.accept(this); _variance = savedVariance; return result; } @override TypeImpl? visitTypeParameterType(covariant TypeParameterTypeImpl node) { if (_variance == Variance.contravariant) { return _replaceTypeParameterTypes(_typeSystem.typeProvider.neverType); } else { var element = node.element; var defaultType = element.defaultType!; return _replaceTypeParameterTypes(defaultType); } } TypeImpl _replaceTypeParameterTypes(TypeImpl type) { return type.accept(this) ?? type; } static TypeImpl replaceTypeVariables( TypeSystemImpl typeSystem, TypeImpl type, ) { return TypeParameterReplacer(typeSystem)._replaceTypeParameterTypes(type); } }