Fix handling of extension types in relational patterns.

In https://dart-review.googlesource.com/c/sdk/+/345082, type erasure
was added to the handling of relational patterns, to address some co19
failures, e.g.:

    extension type const BoolET1(bool _) {}
    const True1 = BoolET1(true);
    String testStatement1(bool b) {
      switch (b) {
        case == True1:
	  ...
      }
    }

This was failing because the type of `True1` (`BoolET1`) is not
assignable to the argument type of `operator==`, which is
`Object`. (This is because extension types do not, by default, extend
`Object`; they extend `Object?`).

Adding type erasure elimited the co19 failure, but it caused other
code to be allowed that shouldn't be allowed, such as:

    extension type const E(int representation) implements Object {}
    class A {
      bool operator <(int other) => ...;
    }
    const E0 = E(0);
    test(A a) {
      if (a case < E0) ...;
    }

This shouldn't be allowed because the type expected by `A.<` is `int`;
allowing `E0` to be passed to this operator breaks extension type
encapsulation.

The correct fix is for assignability checks for `operator==` to use
`S?` rather than `S`, where `S` is the argument type of
`operator==`. This is consistent with the patterns specification, and
it ensures that `== null` and `!= null` are allowed, while continuing
to prohibit relational patterns that break extension type
encapsulation.

Fixes https://github.com/dart-lang/sdk/issues/54594.

Bug: https://github.com/dart-lang/sdk/issues/54594.
Change-Id: Id090f432500d75ba694383f1788d58353cd1fc72
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/345860
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry 2024-01-12 19:31:40 +00:00 committed by Commit Queue
parent e49f1ccbbe
commit 973eca72f5
4 changed files with 227 additions and 22 deletions

View file

@ -1630,15 +1630,15 @@ mixin TypeAnalyzer<
Error? argumentTypeNotAssignableError;
Error? operatorReturnTypeNotAssignableToBoolError;
if (operator != null) {
Type extensionTypeErasure = operations.extensionTypeErasure(operandType);
Type argumentType = isEquality
? operations.promoteToNonNull(extensionTypeErasure)
: extensionTypeErasure;
if (!operations.isAssignableTo(argumentType, operator.parameterType)) {
Type parameterType = operator.parameterType;
if (isEquality) {
parameterType = operations.makeNullable(parameterType);
}
if (!operations.isAssignableTo(operandType, parameterType)) {
argumentTypeNotAssignableError =
errors.relationalPatternOperandTypeNotAssignable(
pattern: node,
operandType: argumentType,
operandType: operandType,
parameterType: operator.parameterType,
);
}

View file

@ -1619,6 +1619,8 @@ class GuardedPattern extends Node with PossiblyGuardedPattern {
class Harness {
static Map<String, Type> _coreMemberTypes = {
'int.<': Type('bool Function(num)'),
'int.<=': Type('bool Function(num)'),
'int.>': Type('bool Function(num)'),
'int.>=': Type('bool Function(num)'),
'num.sign': Type('num'),
@ -1684,6 +1686,12 @@ class Harness {
operations.addExhaustiveness(type, isExhaustive);
}
/// Updates the harness so that when an extension type erasure query is
/// invoked on type [type], [representation] will be returned.
void addExtensionTypeErasure(String type, String representation) {
operations.addExtensionTypeErasure(type, representation);
}
/// Updates the harness so that when member [memberName] is looked up on type
/// [targetType], a member is found having the given [type].
///
@ -2617,6 +2625,7 @@ class MiniAstOperations implements TypeAnalyzerOperations<Var, Type> {
'int, num': Type('num'),
'Never, int': Type('int'),
'Null, int': Type('int?'),
'Null, Object': Type('Object?'),
'?, int': Type('int'),
'?, List<?>': Type('List<?>'),
'?, Null': Type('Null'),
@ -2682,6 +2691,8 @@ class MiniAstOperations implements TypeAnalyzerOperations<Var, Type> {
final Map<String, bool> _exhaustiveness = Map.of(_coreExhaustiveness);
final Map<String, Type> _extensionTypeErasure = {};
final Map<String, Type> _glbs = Map.of(_coreGlbs);
final Map<String, Type> _lubs = Map.of(_coreLubs);
@ -2726,6 +2737,12 @@ class MiniAstOperations implements TypeAnalyzerOperations<Var, Type> {
_exhaustiveness[type] = isExhaustive;
}
/// Updates the harness so that when an extension type erasure query is
/// invoked on type [type], [representation] will be returned.
void addExtensionTypeErasure(String type, String representation) {
_extensionTypeErasure[type] = Type(representation);
}
void addPromotionException(String from, String to, String result) {
(_promotionExceptions[from] ??= {})[to] = result;
}
@ -2783,6 +2800,12 @@ class MiniAstOperations implements TypeAnalyzerOperations<Var, Type> {
fail('Unknown downward inference query: $query');
}
@override
Type extensionTypeErasure(Type type) {
var query = '$type';
return _extensionTypeErasure[query] ?? type;
}
@override
Type factor(Type from, Type what) {
return _typeSystem.factor(from, what);
@ -2944,11 +2967,6 @@ class MiniAstOperations implements TypeAnalyzerOperations<Var, Type> {
);
}
@override
Type extensionTypeErasure(Type type) {
return type;
}
@override
Type streamType(Type elementType) {
return PrimaryType('Stream', args: [elementType]);

View file

@ -3587,19 +3587,129 @@ main() {
'matchedType: Object), variables(), true, block(), noop)')
]);
});
test('argument type not assignable', () {
h.run([
ifCase(
expr('int').checkContext('?'),
relationalPattern('>', expr('String'))..errorId = 'PATTERN',
[],
).checkIR('ifCase(expr(int), >(expr(String), '
'matchedType: int), variables(), true, block(), noop)')
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: String, parameterType: num)'
group('argument type not assignable:', () {
test('basic', () {
h.run([
ifCase(
expr('int').checkContext('?'),
relationalPattern('>', expr('String'))..errorId = 'PATTERN',
[],
).checkIR('ifCase(expr(int), >(expr(String), '
'matchedType: int), variables(), true, block(), noop)')
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: String, parameterType: num)'
});
});
test('> nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('>', expr('int?'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: int?, parameterType: num)'
});
});
test('< nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('<', expr('int?'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: int?, parameterType: num)'
});
});
test('>= nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('>=', expr('int?'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: int?, parameterType: num)'
});
});
test('<= nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('<=', expr('int?'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: int?, parameterType: num)'
});
});
test('extension type to representation', () {
h.addSuperInterfaces('E', (_) => [Type('Object?')]);
h.addExtensionTypeErasure('E', 'int');
h.addMember('C', '>', 'bool Function(int)');
h.run([
ifCase(
expr('C'),
relationalPattern('>', expr('E'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: E, parameterType: int)'
});
});
test('representation to extension type', () {
h.addSuperInterfaces('E', (_) => [Type('Object?')]);
h.addExtensionTypeErasure('E', 'int');
h.addMember('C', '>', 'bool Function(E)');
h.run([
ifCase(
expr('C'),
relationalPattern('>', expr('int'))..errorId = 'PATTERN',
[],
)
], expectedErrors: {
'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
'operandType: int, parameterType: E)'
});
});
});
group('argument type assignable:', () {
test('== nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('==', expr('int?')),
[],
)
]);
});
test('!= nullable', () {
h.run([
ifCase(
expr('int'),
relationalPattern('!=', expr('int?')),
[],
)
]);
});
});
test('return type is not assignable to bool', () {
h.addMember('A', '>', 'int Function(Object)');
h.run([

View file

@ -0,0 +1,77 @@
// 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.
// This test verifies that extension type erasure is not performed
// when checking the type argument of a relational pattern.
extension type const E(int representation) implements Object {}
class A {
bool operator ==(covariant int other) => true;
bool operator <(int other) => true;
bool operator <=(int other) => true;
bool operator >(int other) => true;
bool operator >=(int other) => true;
}
class B {
bool operator ==(covariant E other) => true;
bool operator <(E other) => true;
bool operator <=(E other) => true;
bool operator >(E other) => true;
bool operator >=(E other) => true;
}
const E0 = E(0);
test() {
if (A() case == E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (A() case != E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (A() case > E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (A() case >= E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (A() case < E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (A() case <= E0) {}
// ^^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
if (B() case == 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
if (B() case != 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
if (B() case > 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
if (B() case >= 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
if (B() case < 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
if (B() case <= 0) {}
// ^
// [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
// [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
}