blob: 0c7132e2c58f563b1293ce9b9057914232a02729 [file] [log] [blame]
// 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/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/type_inference/type_analyzer_operations.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: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<DartObject, FieldElement, TypeImpl> {
final List<MissingPatternPart> 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<TypeImpl, EnumElement, FieldElement, DartObject> {
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.name3}.${enumField.name3}';
}
@override
Iterable<FieldElement> 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<TypeImpl, InterfaceElementImpl> {
final TypeSystemImpl _typeSystem;
AnalyzerSealedClassOperations(this._typeSystem);
@override
List<InterfaceElementImpl> getDirectSubclasses(
InterfaceElementImpl sealedClass,
) {
List<InterfaceElementImpl> subclasses = [];
var library = sealedClass.library;
outer:
for (var declaration in library.children2) {
if (declaration is ExtensionTypeElement) {
continue;
}
if (declaration != sealedClass && declaration is InterfaceElementImpl) {
bool checkType(InterfaceTypeImpl? type) {
if (type?.element == sealedClass) {
subclasses.add(declaration);
return true;
}
return false;
}
if (checkType(declaration.supertype)) {
continue outer;
}
for (var mixin in declaration.mixins) {
if (checkType(mixin)) {
continue outer;
}
}
for (var interface in declaration.interfaces) {
if (checkType(interface)) {
continue outer;
}
}
if (declaration is MixinElementImpl) {
for (var type in declaration.superclassConstraints) {
if (checkType(type)) {
continue outer;
}
}
}
}
}
return subclasses;
}
@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.asInstanceOf2(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.typeParameters2,
sealedClassType.typeArguments,
);
for (int i = 0; i < subClass.typeParameters2.length; i++) {
var bound = subClass.typeParameters2[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<TypeImpl> {
final TypeSystemImpl _typeSystem;
final LibraryElement _enclosingLibrary;
final Map<InterfaceTypeImpl, Map<Key, TypeImpl>> _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<Key, TypeImpl> getFieldTypes(TypeImpl type) {
if (type is InterfaceTypeImpl) {
return _getInterfaceFieldTypes(type);
} else if (type is RecordTypeImpl) {
Map<Key, TypeImpl> 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.asInstanceOf2(_typeSystem.typeProvider.listElement);
if (listType != null) {
return listType.typeArguments[0];
}
return null;
}
@override
TypeImpl? getListType(TypeImpl type) {
return type.asInstanceOf2(_typeSystem.typeProvider.listElement);
}
@override
TypeImpl? getMapValueType(TypeImpl type) {
var mapType = type.asInstanceOf2(_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 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
TypeImpl overapproximate(TypeImpl type) {
return TypeParameterReplacer.replaceTypeVariables(_typeSystem, type);
}
@override
String typeToString(TypeImpl type) => type.toString();
Map<Key, TypeImpl> _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.name3;
if (name == null) {
continue;
}
if (!getter.isStatic) {
fieldTypes[NameKey(name)] = getter.type.returnType;
}
}
for (var method in type.methods2) {
if (method.isPrivate && method.library != _enclosingLibrary) {
continue;
}
var name = method.name3;
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<AstNode, StaticType> switchScrutineeType = {};
/// Map from switch statement/expression nodes the spaces for its cases.
Map<AstNode, List<Space>> switchCases = {};
/// Map from switch case nodes to the space for its pattern/expression.
Map<AstNode, Space> caseSpaces = {};
/// Map from unreachable switch case nodes to information about their
/// unreachability.
Map<AstNode, CaseUnreachability> caseUnreachabilities = {};
/// Map from switch statement nodes that are erroneous due to being
/// non-exhaustive, to information about their non-exhaustiveness.
Map<AstNode, NonExhaustiveness> 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.name3!;
}
abstract class MissingPatternPart {}
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<DartPattern, TypeImpl> {
final Version languageVersion;
final FeatureSet featureSet;
final AnalyzerExhaustivenessCache cache;
final Map<Expression, DartObjectImpl> mapPatternKeyValues;
final Map<ConstantPattern, DartObjectImpl> 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<TypeImpl> get typeOperations => cache.typeOperations;
@override
StaticType createListType(
TypeImpl type,
ListTypeRestriction<TypeImpl> restriction,
) {
return cache.getListStaticType(type, restriction);
}
@override
StaticType createMapType(
TypeImpl type,
MapTypeRestriction<TypeImpl> 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.declaredElement!.type,
nonNull: nonNull,
);
} else if (pattern is ObjectPattern) {
var properties = <String, DartPattern>{};
var extensionPropertyTypes = <String, TypeImpl>{};
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 PropertyAccessorElement2OrMember &&
(element.enclosingElement is ExtensionElementImpl ||
element.enclosingElement is ExtensionTypeElementImpl)) {
extensionPropertyType = element.returnType;
} else if (element is ExecutableElement2OrMember &&
(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 = <TypeImpl>[];
var positionalPatterns = <DartPattern>[];
var namedTypes = <String, TypeImpl>{};
var namedPatterns = <String, DartPattern>{};
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<DartPattern> headElements = [];
DartPattern? restElement;
List<DartPattern> 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<MapKey, DartPattern> 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 = <Key, Space>{};
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<DartObjectImpl>(
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);
}
}