Version 2.17.0-138.0.dev

Merge commit '19c7f85bfe1758d39570feeee3e6aa3ebdc56d90' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 225f1ca..3be94d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@
 
 - Add `Finalizer` and `WeakReference` which can potentially detect when
   objects are "garbage collected".
+- Add `isMimeType` method to `UriData` class, to allow case-insensitive
+  checking of the MIME type.
+- Add `isCharset` and `isEncoding` methods to `UriData` class,
+  to allow case-insensitive and alternative-encoding-name aware checking
+  of the MIME type "charset" parameter.
+- Make `UriData.fromString` and `UriData.fromBytes` recognize and omit
+  a "text/plain" `mimeType` even if it is not all lower-case.
 
 #### `dart:ffi`
 
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/enum_from_lib_used_as_type.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/enum_from_lib_used_as_type.dart.expect
index 2f5ecea..bdf5f22 100644
--- a/pkg/vm/testcases/transformations/type_flow/transformer/enum_from_lib_used_as_type.dart.expect
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/enum_from_lib_used_as_type.dart.expect
@@ -22,6 +22,6 @@
   synthetic constructor •() → self::Class
     : super core::Object::•()
     ;
-[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3331,getterSelectorId:3332]  method method([@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::Enum e) → core::int
+[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3333,getterSelectorId:3334]  method method([@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::Enum e) → core::int
     return [@vm.inferred-type.metadata=!] e.{core::_Enum::index}{core::int};
 }
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/tree_shake_enum_from_lib.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/tree_shake_enum_from_lib.dart.expect
index 512caf3..0a305ef 100644
--- a/pkg/vm/testcases/transformations/type_flow/transformer/tree_shake_enum_from_lib.dart.expect
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/tree_shake_enum_from_lib.dart.expect
@@ -51,6 +51,6 @@
   synthetic constructor •() → self::ConstClass
     : super core::Object::•()
     ;
-[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3335,getterSelectorId:3336]  method method([@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::ConstEnum e) → core::int
+[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3337,getterSelectorId:3338]  method method([@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::ConstEnum e) → core::int
     return [@vm.inferred-type.metadata=!] e.{core::_Enum::index}{core::int};
 }
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart
index d8ee831..f74f55e 100644
--- a/sdk/lib/core/uri.dart
+++ b/sdk/lib/core/uri.dart
@@ -1715,38 +1715,7 @@
     String thisScheme = this.scheme;
     if (scheme == null) return thisScheme.isEmpty;
     if (scheme.length != thisScheme.length) return false;
-    return _compareScheme(scheme, thisScheme);
-  }
-
-  /// Compares scheme characters in [scheme] and at the start of [uri].
-  ///
-  /// Returns `true` if [scheme] represents the same scheme as the start of
-  /// [uri]. That means having the same characters, but possibly different case
-  /// for letters.
-  ///
-  /// This function doesn't check that the characters are valid URI scheme
-  /// characters. The [uri] is assumed to be valid, so if [scheme] matches
-  /// it, it has to be valid too.
-  ///
-  /// The length should be tested before calling this function,
-  /// so the scheme part of [uri] is known to have the same length as [scheme].
-  static bool _compareScheme(String scheme, String uri) {
-    for (int i = 0; i < scheme.length; i++) {
-      int schemeChar = scheme.codeUnitAt(i);
-      int uriChar = uri.codeUnitAt(i);
-      int delta = schemeChar ^ uriChar;
-      if (delta != 0) {
-        if (delta == 0x20) {
-          // Might be a case difference.
-          int lowerChar = uriChar | delta;
-          if (0x61 /*a*/ <= lowerChar && lowerChar <= 0x7a /*z*/) {
-            continue;
-          }
-        }
-        return false;
-      }
-    }
-    return true;
+    return _caseInsensitiveStartsWith(scheme, thisScheme, 0);
   }
 
   /// Report a parse failure.
@@ -3481,7 +3450,7 @@
       Map<String, String>? parameters,
       StringBuffer buffer,
       List<int>? indices) {
-    if (mimeType == null || mimeType == "text/plain") {
+    if (mimeType == null || _caseInsensitiveEquals("text/plain", mimeType)) {
       mimeType = "";
     }
 
@@ -3499,11 +3468,9 @@
           _tokenCharTable, mimeType.substring(slashIndex + 1), utf8, false));
     }
     if (charsetName != null) {
-      // TODO(39209): Use ?.. when sequences are properly supported.
-      if (indices != null)
-        indices
-          ..add(buffer.length)
-          ..add(buffer.length + 8);
+      indices
+        ?..add(buffer.length)
+        ..add(buffer.length + 8);
       buffer.write(";charset=");
       buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, utf8, false));
     }
@@ -3637,6 +3604,27 @@
     return _Uri._uriDecode(_text, start, end, utf8, false);
   }
 
+  /// Whether the [UriData.mimeType] is equal to [mimeType].
+  ///
+  /// Compares the `data:` URI's MIME type to [mimeType] with a case-
+  /// insensitive comparison which ignores the case of ASCII letters.
+  ///
+  /// An empty [mimeType] is considered equivalent to `text/plain`,
+  /// both in the [mimeType] argument and in the `data:` URI itself.
+  @Since("2.17")
+  bool isMimeType(String mimeType) {
+    int start = _separatorIndices[0] + 1;
+    int end = _separatorIndices[1];
+    if (start == end) {
+      return mimeType.isEmpty ||
+          identical(mimeType, "text/plain") ||
+          _caseInsensitiveEquals(mimeType, "text/plain");
+    }
+    if (mimeType.isEmpty) mimeType = "text/plain";
+    return (mimeType.length == end - start) &&
+        _caseInsensitiveStartsWith(mimeType, _text, start);
+  }
+
   /// The charset parameter of the media type.
   ///
   /// If the parameters of the media type contains a `charset` parameter
@@ -3647,23 +3635,89 @@
   /// If the MIME type representation in the URI text contains URI escapes,
   /// they are unescaped in the returned string.
   String get charset {
-    int parameterStart = 1;
-    int parameterEnd = _separatorIndices.length - 1; // The ',' before data.
-    if (isBase64) {
-      // There is a ";base64" separator, so subtract one for that as well.
-      parameterEnd -= 1;
-    }
-    for (int i = parameterStart; i < parameterEnd; i += 2) {
-      var keyStart = _separatorIndices[i] + 1;
-      var keyEnd = _separatorIndices[i + 1];
-      if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) {
-        return _Uri._uriDecode(
-            _text, keyEnd + 1, _separatorIndices[i + 2], utf8, false);
-      }
+    var charsetIndex = _findCharsetIndex();
+    if (charsetIndex >= 0) {
+      var valueStart = _separatorIndices[charsetIndex + 1] + 1;
+      var valueEnd = _separatorIndices[charsetIndex + 2];
+      return _Uri._uriDecode(_text, valueStart, valueEnd, utf8, false);
     }
     return "US-ASCII";
   }
 
+  /// Finds the index of the separator before the "charset" parameter.
+  ///
+  /// Returns the index in [_separatorIndices] of the separator before
+  /// the name of the "charset" parameter, or -1 if there is no "charset"
+  /// parameter.
+  int _findCharsetIndex() {
+    var separatorIndices = _separatorIndices;
+    // Loop over all MIME-type parameters.
+    // Check that the parameter can have two parts (key/value)
+    // to ignore a trailing base-64 marker.
+    for (int i = 3; i <= separatorIndices.length; i += 2) {
+      var keyStart = separatorIndices[i - 2] + 1;
+      var keyEnd = separatorIndices[i - 1];
+      if (keyEnd == keyStart + "charset".length &&
+          _caseInsensitiveStartsWith("charset", _text, keyStart)) {
+        return i - 2;
+      }
+    }
+    return -1;
+  }
+
+  /// Checks whether the charset parameter of the mime type is [charset].
+  ///
+  /// If this URI has no "charset" parameter, it is assumed to have a default
+  /// of `charset=US-ASCII`.
+  /// If [charset] is empty, it's treated like `"US-ASCII"`.
+  ///
+  /// Returns true if [charset] and the "charset" parameter value are
+  /// equal strings, ignoring the case of ASCII letters, or both
+  /// correspond to the same [Encoding], as given by [Encoding.getByName].
+  @Since("2.17")
+  bool isCharset(String charset) {
+    var charsetIndex = _findCharsetIndex();
+    if (charsetIndex < 0) {
+      return charset.isEmpty ||
+          _caseInsensitiveEquals(charset, "US-ASCII") ||
+          identical(Encoding.getByName(charset), ascii);
+    }
+    if (charset.isEmpty) charset = "US-ASCII";
+    var valueStart = _separatorIndices[charsetIndex + 1] + 1;
+    var valueEnd = _separatorIndices[charsetIndex + 2];
+    var length = valueEnd - valueStart;
+    if (charset.length == length &&
+        _caseInsensitiveStartsWith(charset, _text, valueStart)) {
+      return true;
+    }
+    var checkedEncoding = Encoding.getByName(charset);
+    return checkedEncoding != null &&
+        identical(
+            checkedEncoding,
+            Encoding.getByName(
+                _Uri._uriDecode(_text, valueStart, valueEnd, utf8, false)));
+  }
+
+  /// Whether the charset parameter represents [encoding].
+  ///
+  /// If the "charset" parameter is not present in the URI,
+  /// it defaults to "US-ASCII", which is the [ascii] encoding.
+  /// If present, it's converted to an [Encoding] using [Encoding.getByName],
+  /// and compared to [encoding].
+  @Since("2.17")
+  bool isEncoding(Encoding encoding) {
+    var charsetIndex = _findCharsetIndex();
+    if (charsetIndex < 0) {
+      return identical(encoding, ascii);
+    }
+    var valueStart = _separatorIndices[charsetIndex + 1] + 1;
+    var valueEnd = _separatorIndices[charsetIndex + 2];
+    return identical(
+        encoding,
+        Encoding.getByName(
+            _Uri._uriDecode(_text, valueStart, valueEnd, utf8, false)));
+  }
+
   /// Whether the data is Base64 encoded or not.
   bool get isBase64 => _separatorIndices.length.isOdd;
 
@@ -4358,7 +4412,7 @@
   bool isScheme(String scheme) {
     if (scheme == null || scheme.isEmpty) return _schemeEnd < 0;
     if (scheme.length != _schemeEnd) return false;
-    return _Uri._compareScheme(scheme, _uri);
+    return _caseInsensitiveStartsWith(scheme, _uri, 0);
   }
 
   String get scheme {
@@ -4857,3 +4911,58 @@
   }
   return -1;
 }
+
+/// Whether [string] at [start] starts with  [prefix], ignoring case.
+///
+/// Returns whether [string] at offset [start]
+/// starts with the characters of [prefix],
+/// but ignores differences in the cases of ASCII letters,
+/// so `a` and `A` are considered equal.
+///
+/// The [string] must be at least as long as [prefix].
+///
+/// When used to checks the schemes of URIs,
+/// this function doesn't check that the characters are valid URI scheme
+/// characters. The [string] is assumed to be a valid URI,
+/// so if [prefix] matches it, it has to be valid too.
+bool _caseInsensitiveStartsWith(String prefix, String string, int start) =>
+    _caseInsensitiveCompareStart(prefix, string, start) >= 0;
+
+/// Compares [string] at [start] with [prefix], ignoring case.
+///
+/// Returns 0 if [string] starts with [prefix] at offset [start].
+/// Returns 0x20 if [string] starts with [prefix] at offset [start],
+/// but some ASCII letters have different case.
+/// Returns a negative value if [string] does not start with [prefix],
+/// at offset [start] even ignoring case differences.
+///
+/// The [string] must be at least as long as `start + prefix.length`.
+int _caseInsensitiveCompareStart(String prefix, String string, int start) {
+  int result = 0;
+  for (int i = 0; i < prefix.length; i++) {
+    int prefixChar = prefix.codeUnitAt(i);
+    int stringChar = string.codeUnitAt(start + i);
+    int delta = prefixChar ^ stringChar;
+    if (delta != 0) {
+      if (delta == 0x20) {
+        // Might be a case difference.
+        int lowerChar = stringChar | delta;
+        if (0x61 /*a*/ <= lowerChar && lowerChar <= 0x7a /*z*/) {
+          result = 0x20;
+          continue;
+        }
+      }
+      return -1;
+    }
+  }
+  return result;
+}
+
+/// Checks whether two strings are equal ignoring case differences.
+///
+/// Returns whether if [string1] and [string2] has the same length
+/// and same characters, but ignores the cases of ASCII letters,
+/// so `a` and `A` are considered equal.
+bool _caseInsensitiveEquals(String string1, String string2) =>
+    string1.length == string2.length &&
+    _caseInsensitiveStartsWith(string1, string2, 0);
diff --git a/tests/corelib/data_uri_test.dart b/tests/corelib/data_uri_test.dart
index 09ff01b..e8636f3 100644
--- a/tests/corelib/data_uri_test.dart
+++ b/tests/corelib/data_uri_test.dart
@@ -34,20 +34,61 @@
 }
 
 void testMediaType() {
-  for (var mimeType in ["", "text/plain", "text/javascript"]) {
-    for (var charset in ["", ";charset=US-ASCII", ";charset=UTF-8"]) {
+  for (var mimeType in ["", "text/plain", "Text/PLAIN", "text/javascript"]) {
+    for (var charset in ["", "US-ASCII", "UTF-8"]) {
       for (var base64 in ["", ";base64"]) {
         bool isBase64 = base64.isNotEmpty;
-        var text = "data:$mimeType$charset$base64,";
+        // Parsing the URI from source:
+        var charsetParameter = charset.isEmpty ? "" : ";charset=$charset";
+        var text = "data:$mimeType$charsetParameter$base64,";
         var uri = UriData.parse(text);
 
-        String expectedCharset =
-            charset.isEmpty ? "US-ASCII" : charset.substring(9);
+        String expectedCharset = charset.isEmpty ? "US-ASCII" : charset;
         String expectedMimeType = mimeType.isEmpty ? "text/plain" : mimeType;
 
         Expect.equals(text, "$uri");
         Expect.equals(expectedMimeType, uri.mimeType);
+        Expect.isTrue(uri.isMimeType(expectedMimeType));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toUpperCase()));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toLowerCase()));
         Expect.equals(expectedCharset, uri.charset);
+        Expect.isTrue(uri.isCharset(expectedCharset));
+        Expect.isTrue(uri.isCharset(expectedCharset.toLowerCase()));
+        Expect.isTrue(uri.isCharset(expectedCharset.toUpperCase()));
+        var expectedEncoding = Encoding.getByName(expectedCharset);
+        if (expectedEncoding != null) {
+          Expect.isTrue(uri.isEncoding(expectedEncoding));
+        }
+        Expect.equals(isBase64, uri.isBase64);
+
+        // Creating the URI using a constructor:
+        var encoding = Encoding.getByName(charset);
+        uri = UriData.fromString("",
+            mimeType: mimeType, encoding: encoding, base64: isBase64);
+        expectedMimeType =
+            (mimeType.isEmpty || mimeType.toLowerCase() == "text/plain")
+                ? "text/plain"
+                : mimeType;
+        expectedEncoding = encoding;
+        expectedCharset = expectedEncoding?.name ?? "US-ASCII";
+        var expectedText = "data:"
+            "${expectedMimeType == "text/plain" ? "" : expectedMimeType}"
+            "${charset.isEmpty ? "" : ";charset=$expectedCharset"}"
+            "${isBase64 ? ";base64" : ""}"
+            ",";
+
+        Expect.equals(expectedText, "$uri");
+        Expect.equals(expectedMimeType, uri.mimeType);
+        Expect.isTrue(uri.isMimeType(expectedMimeType));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toUpperCase()));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toLowerCase()));
+        Expect.equals(expectedCharset, uri.charset);
+        Expect.isTrue(uri.isCharset(expectedCharset));
+        Expect.isTrue(uri.isCharset(expectedCharset.toLowerCase()));
+        Expect.isTrue(uri.isCharset(expectedCharset.toUpperCase()));
+        if (expectedEncoding != null) {
+          Expect.isTrue(uri.isEncoding(expectedEncoding));
+        }
         Expect.equals(isBase64, uri.isBase64);
       }
     }
@@ -236,8 +277,8 @@
   Expect.throwsFormatException(() => UriData.parse("data:type/sub;k=v;base64"));
 
   void formatError(String input) {
-    Expect.throwsFormatException(() => UriData.parse("data:;base64,$input"),
-        input);
+    Expect.throwsFormatException(
+        () => UriData.parse("data:;base64,$input"), input);
   }
 
   // Invalid base64 format (detected when parsed).
diff --git a/tests/corelib_2/data_uri_test.dart b/tests/corelib_2/data_uri_test.dart
index 4e194c1..ddff614 100644
--- a/tests/corelib_2/data_uri_test.dart
+++ b/tests/corelib_2/data_uri_test.dart
@@ -36,20 +36,61 @@
 }
 
 void testMediaType() {
-  for (var mimeType in ["", "text/plain", "text/javascript"]) {
-    for (var charset in ["", ";charset=US-ASCII", ";charset=UTF-8"]) {
+  for (var mimeType in ["", "text/plain", "Text/PLAIN", "text/javascript"]) {
+    for (var charset in ["", "US-ASCII", "UTF-8"]) {
       for (var base64 in ["", ";base64"]) {
         bool isBase64 = base64.isNotEmpty;
-        var text = "data:$mimeType$charset$base64,";
+        // Parsing the URI from source:
+        var charsetParameter = charset.isEmpty ? "" : ";charset=$charset";
+        var text = "data:$mimeType$charsetParameter$base64,";
         var uri = UriData.parse(text);
 
-        String expectedCharset =
-            charset.isEmpty ? "US-ASCII" : charset.substring(9);
+        String expectedCharset = charset.isEmpty ? "US-ASCII" : charset;
         String expectedMimeType = mimeType.isEmpty ? "text/plain" : mimeType;
 
         Expect.equals(text, "$uri");
         Expect.equals(expectedMimeType, uri.mimeType);
+        Expect.isTrue(uri.isMimeType(expectedMimeType));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toUpperCase()));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toLowerCase()));
         Expect.equals(expectedCharset, uri.charset);
+        Expect.isTrue(uri.isCharset(expectedCharset));
+        Expect.isTrue(uri.isCharset(expectedCharset.toLowerCase()));
+        Expect.isTrue(uri.isCharset(expectedCharset.toUpperCase()));
+        var expectedEncoding = Encoding.getByName(expectedCharset);
+        if (expectedEncoding != null) {
+          Expect.isTrue(uri.isEncoding(expectedEncoding));
+        }
+        Expect.equals(isBase64, uri.isBase64);
+
+        // Creating the URI using a constructor:
+        var encoding = Encoding.getByName(charset);
+        uri = UriData.fromString("",
+            mimeType: mimeType, encoding: encoding, base64: isBase64);
+        expectedMimeType =
+            (mimeType.isEmpty || mimeType.toLowerCase() == "text/plain")
+                ? "text/plain"
+                : mimeType;
+        expectedEncoding = encoding;
+        expectedCharset = expectedEncoding?.name ?? "US-ASCII";
+        var expectedText = "data:"
+            "${expectedMimeType == "text/plain" ? "" : expectedMimeType}"
+            "${charset.isEmpty ? "" : ";charset=$expectedCharset"}"
+            "${isBase64 ? ";base64" : ""}"
+            ",";
+
+        Expect.equals(expectedText, "$uri");
+        Expect.equals(expectedMimeType, uri.mimeType);
+        Expect.isTrue(uri.isMimeType(expectedMimeType));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toUpperCase()));
+        Expect.isTrue(uri.isMimeType(expectedMimeType.toLowerCase()));
+        Expect.equals(expectedCharset, uri.charset);
+        Expect.isTrue(uri.isCharset(expectedCharset));
+        Expect.isTrue(uri.isCharset(expectedCharset.toLowerCase()));
+        Expect.isTrue(uri.isCharset(expectedCharset.toUpperCase()));
+        if (expectedEncoding != null) {
+          Expect.isTrue(uri.isEncoding(expectedEncoding));
+        }
         Expect.equals(isBase64, uri.isBase64);
       }
     }
@@ -238,8 +279,8 @@
   Expect.throwsFormatException(() => UriData.parse("data:type/sub;k=v;base64"));
 
   void formatError(String input) {
-    Expect.throwsFormatException(() => UriData.parse("data:;base64,$input"),
-        input);
+    Expect.throwsFormatException(
+        () => UriData.parse("data:;base64,$input"), input);
   }
 
   // Invalid base64 format (detected when parsed).
diff --git a/tests/language/call/implicit_tearoff_local_assignment_test.dart b/tests/language/call/implicit_tearoff_local_assignment_test.dart
new file mode 100644
index 0000000..0f77761
--- /dev/null
+++ b/tests/language/call/implicit_tearoff_local_assignment_test.dart
@@ -0,0 +1,123 @@
+// Copyright (c) 2022, 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 when considering whether to perform a `.call` tearoff
+// on the RHS of an assignment, the implementations use the unpromoted type of
+// the variable (rather than the promoted type).
+
+// NOTICE: This test checks the currently implemented behavior, even though the
+// implemented behavior does not match the language specification.  Until an
+// official decision has been made about whether to change the implementation to
+// match the specification, or vice versa, this regression test is intended to
+// protect against inadvertent implementation changes.
+
+import "package:expect/expect.dart";
+
+import '../static_type_helper.dart';
+
+class B {
+  Object call() => 'B.call called';
+}
+
+class C extends B {
+  String call() => 'C.call called';
+}
+
+void testClassPromoted() {
+  B x = C();
+  x as C;
+  x.expectStaticType<Exactly<C>>();
+  var y = x = C(); // No implicit tearoff of `.call`, no demotion
+  x.expectStaticType<Exactly<C>>();
+  Expect.type<C>(x);
+  Expect.equals('C.call called', x());
+  y.expectStaticType<Exactly<C>>();
+  Expect.type<C>(y);
+  Expect.equals('C.call called', y());
+}
+
+void testClassUnpromoted() {
+  B x = B();
+  var y = x = C(); // No implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<B>>();
+  Expect.type<C>(x);
+  Expect.equals('C.call called', x());
+  y.expectStaticType<Exactly<C>>();
+  Expect.type<C>(y);
+  Expect.equals('C.call called', y());
+}
+
+void testFunctionPromoted() {
+  String f() => 'f called';
+  Object Function() x = f;
+  x as String Function();
+  x.expectStaticType<Exactly<String Function()>>();
+  var y = x = C(); // Implicit tearoff of `.call`, no demotion
+  x.expectStaticType<Exactly<String Function()>>();
+  Expect.type<String Function()>(x);
+  Expect.equals('C.call called', x());
+  y.expectStaticType<Exactly<String Function()>>();
+  Expect.type<String Function()>(y);
+  Expect.equals('C.call called', y());
+}
+
+void testFunctionUnpromoted() {
+  Object f() => 'f called';
+  Object Function() x = f;
+  var y = x = B(); // Implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<Object Function()>>();
+  Expect.type<Object Function()>(x);
+  Expect.equals('B.call called', x());
+  y.expectStaticType<Exactly<Object Function()>>();
+  Expect.type<Object Function()>(y);
+  Expect.equals('B.call called', y());
+}
+
+void testObjectPromotedToClass() {
+  Object x = B();
+  x as B;
+  x.expectStaticType<Exactly<B>>();
+  var y = x = C(); // No implicit tearoff of `.call`, x remains promoted
+  x.expectStaticType<Exactly<B>>();
+  Expect.type<C>(x);
+  Expect.equals('C.call called', x());
+  y.expectStaticType<Exactly<C>>();
+  Expect.type<C>(y);
+  Expect.equals('C.call called', y());
+}
+
+void testObjectPromotedToFunction() {
+  Object f() => 'f called';
+  Object x = f;
+  x as Object Function();
+  x.expectStaticType<Exactly<Object Function()>>();
+  var y = x = B(); // No implicit tearoff of `.call`, demotes x
+  x.expectStaticType<Exactly<Object>>();
+  Expect.type<B>(x);
+  Expect.equals('B.call called', (x as B)());
+  y.expectStaticType<Exactly<B>>();
+  Expect.type<B>(y);
+  Expect.equals('B.call called', y());
+}
+
+void testObjectUnpromoted() {
+  Object x = 'initial value';
+  var y = x = B(); // No implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<Object>>();
+  Expect.type<B>(x);
+  Expect.equals('B.call called', (x as B)());
+  y.expectStaticType<Exactly<B>>();
+  Expect.type<B>(y);
+  Expect.equals('B.call called', y());
+}
+
+main() {
+  testClassPromoted();
+  testClassUnpromoted();
+  testFunctionPromoted();
+  testFunctionUnpromoted();
+  testObjectPromotedToClass();
+  testObjectPromotedToFunction();
+  testObjectUnpromoted();
+}
diff --git a/tests/language_2/call/implicit_tearoff_local_assignment_test.dart b/tests/language_2/call/implicit_tearoff_local_assignment_test.dart
new file mode 100644
index 0000000..210ace0
--- /dev/null
+++ b/tests/language_2/call/implicit_tearoff_local_assignment_test.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2022, 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 is the pre-null-safety variant of the null safety test
+// `implicit_tearoff_local_assignment_test.dart`, which verifies that when
+// considering whether to perform a `.call` tearoff on the RHS of an assignment,
+// the implementations use the unpromoted type of the variable (rather than the
+// promoted type).  For the pre-null-safety variant, the same logic doesn't
+// really apply, because a variable can't be promoted in a block that contains
+// an assignment to it.  But we can still test the unpromoted cases.
+
+// @dart = 2.9
+
+import "package:expect/expect.dart";
+
+import '../static_type_helper.dart';
+
+class B {
+  Object call() => 'B.call called';
+}
+
+class C extends B {
+  String call() => 'C.call called';
+}
+
+void testClassUnpromoted() {
+  B x = B();
+  var y = x = C(); // No implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<B>>();
+  Expect.type<C>(x);
+  Expect.equals('C.call called', x());
+  y.expectStaticType<Exactly<C>>();
+  Expect.type<C>(y);
+  Expect.equals('C.call called', y());
+}
+
+void testFunctionUnpromoted() {
+  Object f() => 'f called';
+  Object Function() x = f;
+  var y = x = B(); // Implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<Object Function()>>();
+  Expect.type<Object Function()>(x);
+  Expect.equals('B.call called', x());
+  y.expectStaticType<Exactly<Object Function()>>();
+  Expect.type<Object Function()>(y);
+  Expect.equals('B.call called', y());
+}
+
+void testObjectUnpromoted() {
+  Object x = 'initial value';
+  var y = x = B(); // No implicit tearoff of `.call`, no promotion
+  x.expectStaticType<Exactly<Object>>();
+  Expect.type<B>(x);
+  Expect.equals('B.call called', (x as B)());
+  y.expectStaticType<Exactly<B>>();
+  Expect.type<B>(y);
+  Expect.equals('B.call called', y());
+}
+
+main() {
+  testClassUnpromoted();
+  testFunctionUnpromoted();
+  testObjectUnpromoted();
+}
diff --git a/tools/VERSION b/tools/VERSION
index 2889eb3..029a3c9 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 17
 PATCH 0
-PRERELEASE 137
+PRERELEASE 138
 PRERELEASE_PATCH 0
\ No newline at end of file