[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);
+  });
+}