[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 https://github.com/dart-lang/sdk/issues/59933.
Bug: https://github.com/dart-lang/sdk/issues/59933
Change-Id: I1da555d65f5d923c809d49a5c73c6d4c2837db18
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/405060
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Auto-Submit: Paul Berry <paulberry@google.com>
diff --git a/pkg/analyzer/lib/src/dart/element/type_system.dart b/pkg/analyzer/lib/src/dart/element/type_system.dart
index 0ebeb45..2575f08 100644
--- a/pkg/analyzer/lib/src/dart/element/type_system.dart
+++ b/pkg/analyzer/lib/src/dart/element/type_system.dart
@@ -575,18 +575,19 @@
parameters ??= <TypeParameterElement>[];
parameters!.add(element);
}
- } else {
- if (type is FunctionType) {
- assert(!type.typeFormals.any((t) => boundTypeParameters.contains(t)));
- boundTypeParameters.addAll(type.typeFormals);
- appendParameters(type.returnType);
- type.parameters.map((p) => p.type).forEach(appendParameters);
- // TODO(scheglov): https://github.com/dart-lang/sdk/issues/44218
- type.alias?.typeArguments.forEach(appendParameters);
- boundTypeParameters.removeAll(type.typeFormals);
- } else if (type is InterfaceType) {
- type.typeArguments.forEach(appendParameters);
- }
+ } else if (type is FunctionType) {
+ assert(!type.typeFormals.any((t) => boundTypeParameters.contains(t)));
+ boundTypeParameters.addAll(type.typeFormals);
+ appendParameters(type.returnType);
+ type.parameters.map((p) => p.type).forEach(appendParameters);
+ // TODO(scheglov): https://github.com/dart-lang/sdk/issues/44218
+ type.alias?.typeArguments.forEach(appendParameters);
+ boundTypeParameters.removeAll(type.typeFormals);
+ } else if (type is InterfaceType) {
+ type.typeArguments.forEach(appendParameters);
+ } else if (type is RecordType) {
+ type.positionalFields.map((f) => f.type).forEach(appendParameters);
+ type.namedFields.map((f) => f.type).forEach(appendParameters);
}
}
diff --git a/pkg/analyzer/test/src/dart/resolution/type_inference/inference_update_1_test.dart b/pkg/analyzer/test/src/dart/resolution/type_inference/inference_update_1_test.dart
index d6c6e81..b765372 100644
--- a/pkg/analyzer/test/src/dart/resolution/type_inference/inference_update_1_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/type_inference/inference_update_1_test.dart
@@ -33,6 +33,34 @@
Feature.inference_update_1.enableString,
];
}
+
+ test_record_field_named() async {
+ // A round of horizontal inference should occur between the first argument
+ // and the second, so that `s.length` is properly resolved.
+ await assertNoErrorsInCode(r'''
+void f<T>(({T x}) v, void Function(T) fn) {
+ fn(v.x);
+}
+test() {
+ f((x: ''), (s) { s.length; });
+}
+''');
+ assertType(findNode.simple('s.length'), 'String');
+ }
+
+ test_record_field_unnamed() async {
+ // A round of horizontal inference should occur between the first argument
+ // and the second, so that `s.length` is properly resolved.
+ await assertNoErrorsInCode(r'''
+void f<T>((T,) v, void Function(T) fn) {
+ fn(v.$1);
+}
+test() {
+ f(('',), (s) { s.length; });
+}
+''');
+ assertType(findNode.simple('s.length'), 'String');
+ }
}
mixin HorizontalInferenceTestCases on PubPackageResolutionTest {
diff --git a/tests/language/inference_update_1/horizontal_inference_record_type_test.dart b/tests/language/inference_update_1/horizontal_inference_record_type_test.dart
new file mode 100644
index 0000000..b1da50f
--- /dev/null
+++ b/tests/language/inference_update_1/horizontal_inference_record_type_test.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2025, 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.
+
+// Tests that horizontal inference works properly when record types are
+// involved. This is an important corner case because record types were added to
+// the language after horizontal inference, and the logic to determine whether a
+// given type variable appears free in a given type was not properly updated to
+// understand record types (see https://github.com/dart-lang/sdk/issues/59933).
+
+import 'package:expect/static_type_helper.dart';
+
+testUnnamedField(void Function<T>((T,) v, void Function(T) fn) f) {
+ // There should be a round of horizontal inference between type inference of
+ // `('',)` and type inference of `(s) { ... }`, because the type `T` appears
+ // free in the type of the former `(T,)` and in the parameter type of the type
+ // of latter (`void Function(T)`).
+ f(('',), (s) {
+ (s..expectStaticType<Exactly<String>>()).length;
+ });
+}
+
+testNamedField(void Function<T>(({T x}) v, void Function(T) fn) f) {
+ // There should be a round of horizontal inference between type inference of
+ // `('',)` and type inference of `(s) { ... }`, because the type `T` appears
+ // free in the type of the former `({T x})` and in the parameter type of the
+ // type of latter (`void Function(T)`).
+ f((x: ''), (s) {
+ (s..expectStaticType<Exactly<String>>()).length;
+ });
+}
+
+main() {
+ testUnnamedField(<T>((T,) v, void Function(T) fn) {
+ fn(v.$1);
+ });
+ testNamedField(<T>(({T x}) v, void Function(T) fn) {
+ fn(v.x);
+ });
+}