blob: c860637f1a91deed00e90c7146051a37274a6ede [file] [log] [blame]
// Copyright (c) 2024, 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/type_inference/nullability_suffix.dart';
import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer_operations.dart';
import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
import '../mini_ast.dart';
import '../mini_types.dart';
main() {
setUp(() {
TypeRegistry.init();
TypeRegistry.addInterfaceTypeName('Contravariant');
TypeRegistry.addInterfaceTypeName('Invariant');
TypeRegistry.addInterfaceTypeName('MyListOfInt');
TypeRegistry.addInterfaceTypeName('Unrelated');
});
tearDown(() {
TypeRegistry.uninit();
});
group('performSubtypeConstraintGenerationForFunctionTypes:', () {
test('Matching functions with no parameters', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function()'), Type('void Function()'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).isEmpty();
});
group('Matching functions with positional parameters:', () {
test('None optional', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, String)'), Type('void Function(T, U)'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
test('Some optional on LHS', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, [String])'),
Type('void Function(T, U)'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
});
group('Non-matching functions with positional parameters:', () {
test('Non-matching due to return types', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('int Function(int)'), Type('String Function(int)'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to parameter types', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int)'), Type('void Function(String)'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to optional parameters on RHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function()'), Type('void Function([int])'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to more parameters being required on LHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int)'), Type('void Function([int])'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
});
group('Matching functions with named parameters:', () {
test('None optional', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({required int x, required String y})'),
Type('void Function({required T x, required U y})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
test('Some optional on LHS', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({required int x, String y})'),
Type('void Function({required T x, required U y})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
test('Optional named parameter on LHS', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, {String x})'),
Type('void Function(T)'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int']);
});
test('Extra optional named parameter on LHS', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({String x, int y})'),
Type('void Function({T y})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int']);
});
});
group('Non-matching functions with named parameters:', () {
test('Non-matching due to return types', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('int Function({int x})'), Type('String Function({int x})'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to named parameter types', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({int x})'),
Type('void Function({String x})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to required named parameter on LHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({required int x})'),
Type('void Function()'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to optional named parameter on RHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function()'), Type('void Function({int x})'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to named parameter on RHS, with decoys on LHS',
() {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function({int x, int y})'),
Type('void Function({int z})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
});
test('Matching functions with named and positional parameters', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, {String y})'),
Type('void Function(T, {U y})'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
group('Non-matching functions with named and positional parameters:', () {
test(
'Non-matching due to LHS not accepting optional positional parameter',
() {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, {String x})'),
Type('void Function(int, [String])'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
test('Non-matching due to positional parameter length mismatch', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
Type('void Function(int, {String x})'),
Type('void Function(int, String)'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(null);
check(tcg._constraints).isEmpty();
});
});
});
group('performSubtypeConstraintGenerationForFutureOr:', () {
test('FutureOr matches FutureOr with constraints based on arguments', () {
// `FutureOr<T> <# FutureOr<int>` reduces to `T <# int`
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<T>'), Type('FutureOr<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['T <: int']);
});
test('FutureOr does not match FutureOr because arguments fail to match',
() {
// `FutureOr<int> <# FutureOr<String>` reduces to `int <# String`
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<int>'), Type('FutureOr<String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isFalse();
check(tcg._constraints).isEmpty();
});
test('Future matches FutureOr favoring Future branch', () {
// `Future<int> <# FutureOr<T>` could match in two possible ways:
// - `Future<int> <# Future<T>` (taking the "Future" branch of the
// FutureOr), producing `int <: T`
// - `Future<int> <# T` (taking the "non-Future" branch of the FutureOr),
// producing `Future<int> <: T`
// In cases where both branches produce a constraint, the "Future" branch
// is favored.
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('Future<int>'), Type('FutureOr<T>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['int <: T']);
});
test('Future matches FutureOr preferring to generate constraints', () {
// `Future<_> <# FutureOr<T>` could match in two possible ways:
// - `Future<_> <# Future<T>` (taking the "Future" branch of the
// FutureOr), producing no constraints
// - `Future<_> <# T` (taking the "non-Future" branch of the FutureOr),
// producing `Future<_> <: T`
// In cases where only one branch produces a constraint, that branch is
// favored.
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('Future<_>'), Type('FutureOr<T>'),
leftSchema: true, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['Future<_> <: T']);
});
test('Type matches FutureOr favoring the Future branch', () {
// `T <# FutureOr<int>` could match in two possible ways:
// - `T <# Future<int>` (taking the "Future" branch of the FutureOr),
// producing `T <: Future<int>`
// - `T <# int` (taking the "non-Future" branch of the FutureOr),
// producing `T <: int`
// In cases where both branches produce a constraint, the "Future" branch
// is favored.
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('T'), Type('FutureOr<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['T <: Future<int>']);
});
test('Future matches FutureOr with no constraints', () {
// `Future<int> <# FutureOr<int>` matches (taking the "Future" branch of
// the FutureOr) without generating any constraints.
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('Future<int>'), Type('FutureOr<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).isEmpty();
});
test('Type matches FutureOr favoring the branch that matches', () {
// `List<T> <# FutureOr<List<int>>` could only match by taking the
// "non-Future" branch of the FutureOr, so the constraint `T <: int` is
// produced.
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('List<T>'), Type('FutureOr<List<int>>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['T <: int']);
});
group('Nullable FutureOr on RHS:', () {
test('Does not match, according to spec', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<T>'), Type('FutureOr<int>?'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isFalse();
check(tcg._constraints).isEmpty();
});
test('Matches, according to CFE discrepancy', () {
var tcg = _TypeConstraintGatherer({'T'},
enableDiscrepantObliviousnessOfNullabilitySuffixOfFutureOr: true);
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<T>'), Type('FutureOr<int>?'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['T <: int']);
});
});
group('Nullable FutureOr on LHS:', () {
test('Does not match, according to spec', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<T>?'), Type('FutureOr<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isFalse();
check(tcg._constraints).isEmpty();
});
test('Matches, according to CFE discrepancy', () {
var tcg = _TypeConstraintGatherer({'T'},
enableDiscrepantObliviousnessOfNullabilitySuffixOfFutureOr: true);
check(tcg.performSubtypeConstraintGenerationForFutureOr(
Type('FutureOr<T>?'), Type('FutureOr<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isTrue();
check(tcg._constraints).deepEquals(['T <: int']);
});
});
});
group('performSubtypeConstraintGenerationForTypeDeclarationTypes', () {
group('Same base type on both sides:', () {
test('Covariant, matching', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Map<T, U>'), Type('Map<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
});
test('Covariant, not matching', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Map<T, int>'), Type('Map<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(false);
check(tcg._constraints).isEmpty();
});
test('Contravariant, matching', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
tcg.typeAnalyzerOperations.addVariance(
'Contravariant', [Variance.contravariant, Variance.contravariant]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Contravariant<T, U>'), Type('Contravariant<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(['int <: T', 'String <: U']);
});
test('Contravariant, not matching', () {
var tcg = _TypeConstraintGatherer({'T'});
tcg.typeAnalyzerOperations.addVariance(
'Contravariant', [Variance.contravariant, Variance.contravariant]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Contravariant<T, int>'),
Type('Contravariant<int, String>'),
leftSchema: false,
astNodeForTesting: Node.placeholder()))
.equals(false);
check(tcg._constraints).isEmpty();
});
test('Invariant, matching', () {
var tcg = _TypeConstraintGatherer({'T', 'U'});
tcg.typeAnalyzerOperations
.addVariance('Invariant', [Variance.invariant, Variance.invariant]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Invariant<T, U>'), Type('Invariant<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).unorderedEquals(
['T <: int', 'U <: String', 'int <: T', 'String <: U']);
});
test('Invariant, not matching', () {
var tcg = _TypeConstraintGatherer({'T'});
tcg.typeAnalyzerOperations
.addVariance('Invariant', [Variance.invariant, Variance.invariant]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Invariant<T, int>'), Type('Invariant<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(false);
check(tcg._constraints).isEmpty();
});
test('Unrelated, matchable', () {
// When the variance is "unrelated", type inference doesn't even try to
// match up the type parameters; they are always considered to match.
var tcg = _TypeConstraintGatherer({'T', 'U'});
tcg.typeAnalyzerOperations
.addVariance('Unrelated', [Variance.unrelated, Variance.unrelated]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Unrelated<T, U>'), Type('Unrelated<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).isEmpty();
});
test('Unrelated, not matchable', () {
// When the variance is "unrelated", type inference doesn't even try to
// match up the type parameters; they are always considered to match.
var tcg = _TypeConstraintGatherer({'T'});
tcg.typeAnalyzerOperations
.addVariance('Unrelated', [Variance.unrelated, Variance.unrelated]);
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('Unrelated<T, int>'), Type('Unrelated<int, String>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).isEmpty();
});
});
group('Related types on both sides:', () {
test('No change in type args', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('List<T>'), Type('Iterable<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).deepEquals(['T <: int']);
});
test('Change in type args', () {
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('MyListOfInt'), Type('List<T>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(true);
check(tcg._constraints).deepEquals(['int <: T']);
});
test('LHS nullable', () {
// When the LHS is a nullable type,
// performSubtypeConstraintGenerationForTypeDeclarationTypes considers
// it not to match (this is handled by other parts of the subtyping
// algorithm)
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('List<T>?'), Type('Iterable<int>'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(false);
check(tcg._constraints).isEmpty();
});
test('RHS nullable', () {
// When the RHS is a nullable type,
// performSubtypeConstraintGenerationForTypeDeclarationTypes considers
// it not to match (this is handled by other parts of the subtyping
// algorithm)
var tcg = _TypeConstraintGatherer({'T'});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('List<T>'), Type('Iterable<int>?'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.equals(false);
check(tcg._constraints).isEmpty();
});
});
test('Non-interface type on LHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('void Function()'), Type('int'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isNull();
check(tcg._constraints).isEmpty();
});
test('Non-interface type on RHS', () {
var tcg = _TypeConstraintGatherer({});
check(tcg.performSubtypeConstraintGenerationForTypeDeclarationTypes(
Type('int'), Type('void Function()'),
leftSchema: false, astNodeForTesting: Node.placeholder()))
.isNull();
check(tcg._constraints).isEmpty();
});
});
}
class _TypeConstraintGatherer extends TypeConstraintGenerator<Type,
NamedFunctionParameter, Var, TypeParameter, Type, String, Node>
with
TypeConstraintGeneratorMixin<Type, NamedFunctionParameter, Var,
TypeParameter, Type, String, Node> {
final _typeVariablesBeingConstrained = <TypeParameter>{};
@override
final bool enableDiscrepantObliviousnessOfNullabilitySuffixOfFutureOr;
@override
final MiniAstOperations typeAnalyzerOperations = MiniAstOperations();
final _constraints = <String>[];
_TypeConstraintGatherer(Set<String> typeVariablesBeingConstrained,
{this.enableDiscrepantObliviousnessOfNullabilitySuffixOfFutureOr =
false}) {
for (var typeVariableName in typeVariablesBeingConstrained) {
_typeVariablesBeingConstrained
.add(TypeRegistry.addTypeParameter(typeVariableName));
}
}
@override
TypeConstraintGeneratorState get currentState =>
TypeConstraintGeneratorState(_constraints.length);
@override
List<Type>? getTypeArgumentsAsInstanceOf(Type type, String typeDeclaration) {
// We just have a few cases hardcoded here to make the tests work.
// TODO(paulberry): if this gets too unwieldy, replace it with a more
// general implementation.
switch ((type, typeDeclaration)) {
case (PrimaryType(name: 'List', :var args), 'Iterable'):
// List<T> inherits from Iterable<T>
return args;
case (PrimaryType(name: 'MyListOfInt'), 'List'):
// MyListOfInt inherits from List<int>
return [Type('int')];
case (PrimaryType(name: 'Future'), 'int'):
case (PrimaryType(name: 'int'), 'String'):
case (PrimaryType(name: 'List'), 'Future'):
case (PrimaryType(name: 'String'), 'int'):
// Unrelated types
return null;
default:
throw UnimplementedError(
'getTypeArgumentsAsInstanceOf($type, $typeDeclaration)');
}
}
@override
bool performSubtypeConstraintGenerationInternal(Type p, Type q,
{required bool leftSchema, required Node? astNodeForTesting}) {
// If `P` is `_` then the match holds with no constraints.
if (p is SharedUnknownTypeStructure) {
return true;
}
// If `Q` is `_` then the match holds with no constraints.
if (q is SharedUnknownTypeStructure) {
return true;
}
// If T is a type variable being constrained, then `T <# Q` matches,
// generating the constraint `T <: Q`.
if (typeAnalyzerOperations.matchInferableParameter(SharedTypeView(p))
case var typeVar?
when p.nullabilitySuffix == NullabilitySuffix.none &&
_typeVariablesBeingConstrained.contains(typeVar)) {
_constraints.add('$typeVar <: $q');
return true;
}
// If T is a type variable being constrained, then `P <# T` matches,
// generating the constraint `P <: T`.
if (typeAnalyzerOperations.matchInferableParameter(SharedTypeView(q))
case var typeVar?
when q.nullabilitySuffix == NullabilitySuffix.none &&
_typeVariablesBeingConstrained.contains(typeVar)) {
_constraints.add('$p <: $typeVar');
return true;
}
// If `P` and `Q` are identical types, then the subtype match holds
// under no constraints.
if (p == q) {
return true;
}
bool? result = performSubtypeConstraintGenerationForTypeDeclarationTypes(
p, q,
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting);
if (result != null) {
return result;
}
// Assume for the moment that nothing else matches.
// TODO(paulberry): expand this as needed.
return false;
}
@override
void restoreState(TypeConstraintGeneratorState state) {
_constraints.length = state.count;
}
}