Skip to content

Commit 7f5fc25

Browse files
stereotype441Commit Queue
authored andcommitted
[analyzer] Fix horizontal type inference when there are record types.
When record types were added to Dart, the analyzer's logic for figuring out which type variables appear free in a given type wasn't updated to recursively visit the types appearing inside a record type. Fixing this issue brings the analyzer's behavior in line with the CFE's behavior, and the spec. Fixes #59933. Bug: #59933 Change-Id: I1da555d65f5d923c809d49a5c73c6d4c2837db18 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/405060 Commit-Queue: Konstantin Shcheglov <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]> Auto-Submit: Paul Berry <[email protected]>
1 parent da6dc03 commit 7f5fc25

File tree

3 files changed

+81
-12
lines changed

3 files changed

+81
-12
lines changed

pkg/analyzer/lib/src/dart/element/type_system.dart

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -575,18 +575,19 @@ class TypeSystemImpl implements TypeSystem {
575575
parameters ??= <TypeParameterElement>[];
576576
parameters!.add(element);
577577
}
578-
} else {
579-
if (type is FunctionType) {
580-
assert(!type.typeFormals.any((t) => boundTypeParameters.contains(t)));
581-
boundTypeParameters.addAll(type.typeFormals);
582-
appendParameters(type.returnType);
583-
type.parameters.map((p) => p.type).forEach(appendParameters);
584-
// TODO(scheglov): https://github.com/dart-lang/sdk/issues/44218
585-
type.alias?.typeArguments.forEach(appendParameters);
586-
boundTypeParameters.removeAll(type.typeFormals);
587-
} else if (type is InterfaceType) {
588-
type.typeArguments.forEach(appendParameters);
589-
}
578+
} else if (type is FunctionType) {
579+
assert(!type.typeFormals.any((t) => boundTypeParameters.contains(t)));
580+
boundTypeParameters.addAll(type.typeFormals);
581+
appendParameters(type.returnType);
582+
type.parameters.map((p) => p.type).forEach(appendParameters);
583+
// TODO(scheglov): https://github.com/dart-lang/sdk/issues/44218
584+
type.alias?.typeArguments.forEach(appendParameters);
585+
boundTypeParameters.removeAll(type.typeFormals);
586+
} else if (type is InterfaceType) {
587+
type.typeArguments.forEach(appendParameters);
588+
} else if (type is RecordType) {
589+
type.positionalFields.map((f) => f.type).forEach(appendParameters);
590+
type.namedFields.map((f) => f.type).forEach(appendParameters);
590591
}
591592
}
592593

pkg/analyzer/test/src/dart/resolution/type_inference/inference_update_1_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,34 @@ class HorizontalInferenceEnabledTest extends PubPackageResolutionTest
3333
Feature.inference_update_1.enableString,
3434
];
3535
}
36+
37+
test_record_field_named() async {
38+
// A round of horizontal inference should occur between the first argument
39+
// and the second, so that `s.length` is properly resolved.
40+
await assertNoErrorsInCode(r'''
41+
void f<T>(({T x}) v, void Function(T) fn) {
42+
fn(v.x);
43+
}
44+
test() {
45+
f((x: ''), (s) { s.length; });
46+
}
47+
''');
48+
assertType(findNode.simple('s.length'), 'String');
49+
}
50+
51+
test_record_field_unnamed() async {
52+
// A round of horizontal inference should occur between the first argument
53+
// and the second, so that `s.length` is properly resolved.
54+
await assertNoErrorsInCode(r'''
55+
void f<T>((T,) v, void Function(T) fn) {
56+
fn(v.$1);
57+
}
58+
test() {
59+
f(('',), (s) { s.length; });
60+
}
61+
''');
62+
assertType(findNode.simple('s.length'), 'String');
63+
}
3664
}
3765

3866
mixin HorizontalInferenceTestCases on PubPackageResolutionTest {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Tests that horizontal inference works properly when record types are
6+
// involved. This is an important corner case because record types were added to
7+
// the language after horizontal inference, and the logic to determine whether a
8+
// given type variable appears free in a given type was not properly updated to
9+
// understand record types (see https://github.com/dart-lang/sdk/issues/59933).
10+
11+
import 'package:expect/static_type_helper.dart';
12+
13+
testUnnamedField(void Function<T>((T,) v, void Function(T) fn) f) {
14+
// There should be a round of horizontal inference between type inference of
15+
// `('',)` and type inference of `(s) { ... }`, because the type `T` appears
16+
// free in the type of the former `(T,)` and in the parameter type of the type
17+
// of latter (`void Function(T)`).
18+
f(('',), (s) {
19+
(s..expectStaticType<Exactly<String>>()).length;
20+
});
21+
}
22+
23+
testNamedField(void Function<T>(({T x}) v, void Function(T) fn) f) {
24+
// There should be a round of horizontal inference between type inference of
25+
// `('',)` and type inference of `(s) { ... }`, because the type `T` appears
26+
// free in the type of the former `({T x})` and in the parameter type of the
27+
// type of latter (`void Function(T)`).
28+
f((x: ''), (s) {
29+
(s..expectStaticType<Exactly<String>>()).length;
30+
});
31+
}
32+
33+
main() {
34+
testUnnamedField(<T>((T,) v, void Function(T) fn) {
35+
fn(v.$1);
36+
});
37+
testNamedField(<T>(({T x}) v, void Function(T) fn) {
38+
fn(v.x);
39+
});
40+
}

0 commit comments

Comments
 (0)