Version 2.18.0-170.0.dev

Merge commit '84402e7824bfd9e6c056ca3f89f5eb550c5fb2f5' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a5483d..2670af5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -104,6 +104,11 @@
 
 - Added `dartify` and a number of minor helper functions.
 
+#### `dart:core`
+
+- Allow omitting the `unencodedPath` positional argument to `Uri.http` and
+  `Uri.https` to default to an empty path.
+
 ### Tools
 
 #### Linter
diff --git a/DEPS b/DEPS
index 5a145db..b09d373 100644
--- a/DEPS
+++ b/DEPS
@@ -89,7 +89,7 @@
   "chrome_rev": "19997",
   "cli_util_rev": "b0adbba89442b2ea6fef39c7a82fe79cb31e1168",
   "clock_rev": "f594d86da123015186d5680b0d0e8255c52fc162",
-  "collection_rev": "69766daafbaa8535d1343fb7cd87e713f57c107f",
+  "collection_rev": "f9b433dfc7dba5c8a987b1a8e6a9050292f5582e",
   "convert_rev": "00b251529c074df394b3391c7e3eea3dd9e5778e",
   "crypto_rev": "4297d240b0e1e780ec0a9eab23eaf1ad491f3e68",
   "csslib_rev": "518761b166974537f334dbf264e7f56cb157a96a",
@@ -145,7 +145,7 @@
   "sse_rev": "9a54f1cdd91c8d79a6bf5ef8e849a12756607453",
   "stack_trace_rev": "17f09c2c6845bb31c7c385acecce5befb8527a13",
   "stream_channel_rev": "3fa3e40c75c210d617b8b943b9b8f580e9866a89",
-  "string_scanner_rev": "6579871b528036767b3200b390a3ecef28e4900d",
+  "string_scanner_rev": "c637deb8d998b72a5807afbd06aba8370db725c0",
   "sync_http_rev": "b6bd47965694dddffb6e62fb8a6c12d17c4ae4cd",
   "term_glyph_rev": "d0f205c67ea70eea47b9f41c8440129a72a9c86e",
   "test_descriptor_rev": "5ed5d7f6bf1191592995dcb8eedbbc17df69d386",
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
index a7e9b00..345e79e 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
@@ -6940,6 +6940,16 @@
         r"""Try removing the 'external' keyword or adding a JS interop annotation.""");
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Null> codeJsInteropInvalidStaticClassMemberName =
+    messageJsInteropInvalidStaticClassMemberName;
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const MessageCode messageJsInteropInvalidStaticClassMemberName = const MessageCode(
+    "JsInteropInvalidStaticClassMemberName",
+    problemMessage:
+        r"""JS interop static class members cannot have '.' in their JS name.""");
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
 const Template<
     Message Function(
         String name,
diff --git a/pkg/_js_interop_checks/lib/js_interop_checks.dart b/pkg/_js_interop_checks/lib/js_interop_checks.dart
index 34a6b09..9638d1e 100644
--- a/pkg/_js_interop_checks/lib/js_interop_checks.dart
+++ b/pkg/_js_interop_checks/lib/js_interop_checks.dart
@@ -14,6 +14,7 @@
         messageJsInteropEnclosingClassJSAnnotationContext,
         messageJsInteropExternalExtensionMemberOnTypeInvalid,
         messageJsInteropExternalMemberNotJSAnnotated,
+        messageJsInteropInvalidStaticClassMemberName,
         messageJsInteropNamedParameters,
         messageJsInteropNonExternalConstructor,
         messageJsInteropNonExternalMember,
@@ -274,6 +275,18 @@
         // named parameters.
         _checkNoNamedParameters(procedure.function);
       }
+
+      // JS static methods cannot use a JS name with dots.
+      if (procedure.isStatic && procedure.enclosingClass != null) {
+        String name = getJSName(procedure);
+        if (name.contains('.')) {
+          _diagnosticsReporter.report(
+              messageJsInteropInvalidStaticClassMemberName,
+              procedure.fileOffset,
+              procedure.name.text.length,
+              procedure.fileUri);
+        }
+      }
     }
 
     if (_classHasStaticInteropAnnotation &&
diff --git a/pkg/analysis_server/lib/src/analytics/analytics_manager.dart b/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
index 12fb6cb..9941bc2 100644
--- a/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
+++ b/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
@@ -23,6 +23,12 @@
   /// Record that the set of plugins known to the [pluginManager] has changed.
   void changedPlugins(PluginManager pluginManager);
 
+  /// Record that the given [notification] was received and has been handled.
+  void handledNotificationMessage(
+      {required NotificationMessage notification,
+      required DateTime startTime,
+      required DateTime endTime}) {}
+
   /// Record that the given [response] was sent to the client.
   void sentResponse({required Response response});
 
diff --git a/pkg/analysis_server/lib/src/analytics/google_analytics_manager.dart b/pkg/analysis_server/lib/src/analytics/google_analytics_manager.dart
index 1111d47..ddeef8c 100644
--- a/pkg/analysis_server/lib/src/analytics/google_analytics_manager.dart
+++ b/pkg/analysis_server/lib/src/analytics/google_analytics_manager.dart
@@ -30,6 +30,10 @@
   /// been responded to.
   final Map<String, _RequestData> _completedRequests = {};
 
+  /// A map from the name of a notification to data about all such notifications
+  /// that have been handled.
+  final Map<String, _NotificationData> _completedNotifications = {};
+
   /// Initialize a newly created analytics manager to report to the [analytics]
   /// service.
   GoogleAnalyticsManager(this.analytics);
@@ -40,6 +44,23 @@
   }
 
   @override
+  void handledNotificationMessage(
+      {required NotificationMessage notification,
+      required DateTime startTime,
+      required DateTime endTime}) {
+    var method = notification.method.toString();
+    var requestTime = notification.clientRequestTime;
+    var start = startTime.millisecondsSinceEpoch;
+    var end = endTime.millisecondsSinceEpoch;
+    var data = _completedNotifications.putIfAbsent(
+        method, () => _NotificationData(method));
+    if (requestTime != null) {
+      data.latencyTimes.addValue(start - requestTime);
+    }
+    data.handlingTimes.addValue(end - start);
+  }
+
+  @override
   void sentResponse({required Response response}) {
     var sendTime = DateTime.now();
     _recordResponseData(response.id, sendTime);
@@ -64,6 +85,7 @@
     _sendSessionData(sessionData);
     _sendServerResponseTimes();
     _sendPluginResponseTimes();
+    _sendNotificationHandlingTimes();
 
     analytics.waitForLastPing(timeout: Duration(milliseconds: 200)).then((_) {
       analytics.close();
@@ -122,6 +144,17 @@
     requestData.responseTimes.addValue(responseTime);
   }
 
+  /// Send information about the notifications handled by the server.
+  void _sendNotificationHandlingTimes() {
+    for (var data in _completedNotifications.values) {
+      analytics.sendEvent('language_server', 'notification', parameters: {
+        'latency': data.latencyTimes.toAnalyticsString(),
+        'method': data.method,
+        'duration': data.handlingTimes.toAnalyticsString(),
+      });
+    }
+  }
+
   /// Send information about the response times of plugins.
   void _sendPluginResponseTimes() {
     var responseTimes = PluginManager.pluginResponseTimes;
@@ -177,6 +210,27 @@
   _ActiveRequestData(this.requestName, this.clientRequestTime, this.startTime);
 }
 
+/// Data about the notifications that have been handled that have the same
+/// method.
+class _NotificationData {
+  /// The name of the notifications.
+  final String method;
+
+  /// The percentile calculator for latency times. The _latency time_ is the
+  /// time from when the client sent the request until the time the server
+  /// started processing the request.
+  final PercentileCalculator latencyTimes = PercentileCalculator();
+
+  /// The percentile calculator for handling times. The _handling time_ is the
+  /// time from when the server started processing the notification until the
+  /// handling was complete.
+  final PercentileCalculator handlingTimes = PercentileCalculator();
+
+  /// Initialize a newly create data holder for notifications with the given
+  /// [method].
+  _NotificationData(this.method);
+}
+
 /// Data about the plugins associated with the context roots.
 class _PluginData {
   /// The number of times that plugin information has been recorded.
@@ -219,7 +273,8 @@
   }
 }
 
-/// Data about the requests that have been responded to that have the same name.
+/// Data about the requests that have been responded to that have the same
+/// method.
 class _RequestData {
   /// The name of the requests.
   final String method;
diff --git a/pkg/analysis_server/lib/src/analytics/noop_analytics_manager.dart b/pkg/analysis_server/lib/src/analytics/noop_analytics_manager.dart
index d52e59d..0c08132 100644
--- a/pkg/analysis_server/lib/src/analytics/noop_analytics_manager.dart
+++ b/pkg/analysis_server/lib/src/analytics/noop_analytics_manager.dart
@@ -14,6 +14,12 @@
   void changedPlugins(PluginManager pluginManager) {}
 
   @override
+  void handledNotificationMessage(
+      {required NotificationMessage notification,
+      required DateTime startTime,
+      required DateTime endTime}) {}
+
+  @override
   void sentResponse({required Response response}) {}
 
   @override
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index 28a2360..23f1958 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -374,6 +374,10 @@
               await _handleRequestMessage(message, messageInfo);
             } else if (message is NotificationMessage) {
               await _handleNotificationMessage(message, messageInfo);
+              analyticsManager.handledNotificationMessage(
+                  notification: message,
+                  startTime: startTime,
+                  endTime: DateTime.now());
             } else {
               showErrorMessageToUser('Unknown incoming message type');
             }
diff --git a/pkg/analysis_server/test/src/analytics/google_analytics_manager_test.dart b/pkg/analysis_server/test/src/analytics/google_analytics_manager_test.dart
index 225e33d..453956b 100644
--- a/pkg/analysis_server/test/src/analytics/google_analytics_manager_test.dart
+++ b/pkg/analysis_server/test/src/analytics/google_analytics_manager_test.dart
@@ -4,6 +4,7 @@
 
 import 'dart:convert';
 
+import 'package:analysis_server/lsp_protocol/protocol.dart';
 import 'package:analysis_server/protocol/protocol.dart';
 import 'package:analysis_server/src/analytics/google_analytics_manager.dart';
 import 'package:analysis_server/src/analytics/percentile_calculator.dart';
@@ -41,6 +42,26 @@
     PluginManager.pluginResponseTimes.clear();
   }
 
+  void test_server_notification() {
+    _defaultStartup();
+    manager.handledNotificationMessage(
+        notification: NotificationMessage(
+            clientRequestTime: 2,
+            jsonrpc: '',
+            method: Method.workspace_didCreateFiles),
+        startTime: _now(),
+        endTime: _now());
+    manager.shutdown();
+    analytics.assertEvents([
+      _ExpectedEvent.session(),
+      _ExpectedEvent.notification(parameters: {
+        'latency': _IsPercentiles(),
+        'method': Method.workspace_didCreateFiles.toString(),
+        'duration': _IsPercentiles(),
+      }),
+    ]);
+  }
+
   void test_server_request() {
     _defaultStartup();
     manager.startedRequest(
@@ -160,6 +181,9 @@
       this.value, // ignore: unused_element
       this.parameters});
 
+  _ExpectedEvent.notification({Map<String, Object>? parameters})
+      : this('language_server', 'notification', parameters: parameters);
+
   _ExpectedEvent.pluginRequest({Map<String, Object>? parameters})
       : this('language_server', 'pluginRequest', parameters: parameters);
 
diff --git a/pkg/analyzer/lib/src/dart/micro/utils.dart b/pkg/analyzer/lib/src/dart/micro/utils.dart
index 6908497..fc88c98 100644
--- a/pkg/analyzer/lib/src/dart/micro/utils.dart
+++ b/pkg/analyzer/lib/src/dart/micro/utils.dart
@@ -307,8 +307,7 @@
         length = 0;
       }
       references.add(MatchInfo(offset, length, kind));
-    }
-    if (e!.enclosingElement == element) {
+    } else if (e != null && e.enclosingElement == element) {
       kind = MatchKind.REFERENCE;
       offset = node.offset;
       length = element.nameLength;
diff --git a/pkg/compiler/lib/src/js_backend/native_data.dart b/pkg/compiler/lib/src/js_backend/native_data.dart
index edc6de8..67f6246 100644
--- a/pkg/compiler/lib/src/js_backend/native_data.dart
+++ b/pkg/compiler/lib/src/js_backend/native_data.dart
@@ -650,11 +650,16 @@
   String getFixedBackendName(MemberEntity element) {
     String name = _nativeMemberName[element];
     if (name == null && isJsInteropMember(element)) {
-      // If an element isJsInterop but _isJsInterop is false that means it is
-      // considered interop as the parent class is interop.
-      name = element.isConstructor
-          ? _jsClassNameHelper(element.enclosingClass)
-          : _jsMemberNameHelper(element);
+      if (element.isConstructor) {
+        name = _jsClassNameHelper(element.enclosingClass);
+      } else {
+        name = _jsMemberNameHelper(element);
+        // Top-level static JS interop members can be associated with a dotted
+        // name, if so, fixedBackendName is the last segment.
+        if (element.isTopLevel && name.contains('.')) {
+          name = name.substring(name.lastIndexOf('.') + 1);
+        }
+      }
       _nativeMemberName[element] = name;
     }
     return name;
@@ -705,6 +710,15 @@
         ..write('.')
         ..write(_jsClassNameHelper(element.enclosingClass));
     }
+
+    // Top-level static JS interop members can be associated with a dotted
+    // name, if so, fixedBackendPath includes all but the last segment.
+    final name = _jsMemberNameHelper(element);
+    if (element.isTopLevel && name.contains('.')) {
+      sb
+        ..write('.')
+        ..write(name.substring(0, name.lastIndexOf('.')));
+    }
     return sb.toString();
   }
 
diff --git a/pkg/compiler/lib/src/js_emitter/native_emitter.dart b/pkg/compiler/lib/src/js_emitter/native_emitter.dart
index 491d2b2..40f80f5 100644
--- a/pkg/compiler/lib/src/js_emitter/native_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/native_emitter.dart
@@ -344,7 +344,7 @@
           1, indexOfLastOptionalArgumentInParameters + 1);
     } else {
       // Native methods that are not intercepted must be static.
-      assert(member.isStatic, failedAt(member));
+      assert(member.isStatic || member.isTopLevel, failedAt(member));
       arguments = argumentsBuffer.sublist(
           0, indexOfLastOptionalArgumentInParameters + 1);
       if (_nativeData.isJsInteropMember(member)) {
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 2b3a91d..6fe471c 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -2645,16 +2645,9 @@
 
   js_ast.LiteralString _emitJSInteropStaticMemberName(NamedNode n) {
     if (!usesJSInterop(n)) return null;
-    var name = _annotationName(n, isPublicJSAnnotation);
-    if (name != null) {
-      if (name.contains('.')) {
-        throw UnsupportedError(
-            'static members do not support "." in their names. '
-            'See https://github.com/dart-lang/sdk/issues/27926');
-      }
-    } else {
-      name = getTopLevelName(n);
-    }
+    var name = _annotationName(n, isPublicJSAnnotation) ?? getTopLevelName(n);
+    assert(name != null && !name.contains('.'),
+        'JS interop checker rejects dotted names on static class members');
     return js.escapedString(name, "'");
   }
 
diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status
index 7c33194..4a1486a 100644
--- a/pkg/front_end/messages.status
+++ b/pkg/front_end/messages.status
@@ -562,6 +562,8 @@
 JsInteropExternalExtensionMemberOnTypeInvalid/example: Fail # Web compiler specific
 JsInteropExternalMemberNotJSAnnotated/analyzerCode: Fail # Web compiler specific
 JsInteropExternalMemberNotJSAnnotated/example: Fail # Web compiler specific
+JsInteropInvalidStaticClassMemberName/analyzerCode: Fail
+JsInteropInvalidStaticClassMemberName/example: Fail
 JsInteropJSClassExtendsDartClass/analyzerCode: Fail # Web compiler specific
 JsInteropJSClassExtendsDartClass/example: Fail # Web compiler specific
 JsInteropNamedParameters/analyzerCode: Fail # Web compiler specific
diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml
index 0d6ce5d..829904c 100644
--- a/pkg/front_end/messages.yaml
+++ b/pkg/front_end/messages.yaml
@@ -5173,6 +5173,9 @@
   problemMessage: "JS interop classes do not support operator methods."
   correctionMessage: "Try replacing this with a normal method."
 
+JsInteropInvalidStaticClassMemberName:
+  problemMessage: "JS interop static class members cannot have '.' in their JS name."
+
 JsInteropStaticInteropWithInstanceMembers:
   problemMessage: "JS interop class '#name' with `@staticInterop` annotation cannot declare instance members."
   correctionMessage: "Try moving the instance member to a static extension."
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart
index f74f55e..bb84862 100644
--- a/sdk/lib/core/uri.dart
+++ b/sdk/lib/core/uri.dart
@@ -238,11 +238,13 @@
   /// The `path` component is set from the [unencodedPath]
   /// argument. The path passed must not be encoded as this constructor
   /// encodes the path.
+  /// If omitted, the path defaults to being empty.
   ///
   /// The `query` component is set from the optional [queryParameters]
   /// argument.
-  factory Uri.http(String authority, String unencodedPath,
-      [Map<String, dynamic>? queryParameters]) = _Uri.http;
+  factory Uri.http(String authority,
+      [String unencodedPath,
+      Map<String, dynamic>? queryParameters]) = _Uri.http;
 
   /// Creates a new `https` URI from authority, path and query.
   ///
@@ -263,8 +265,9 @@
   /// uri = Uri.https('example.org', '/a%2F');
   /// print(uri); // https://example.org/a%252F
   /// ```
-  factory Uri.https(String authority, String unencodedPath,
-      [Map<String, dynamic>? queryParameters]) = _Uri.https;
+  factory Uri.https(String authority,
+      [String unencodedPath,
+      Map<String, dynamic>? queryParameters]) = _Uri.https;
 
   /// Creates a new file URI from an absolute or relative file path.
   ///
@@ -1667,14 +1670,14 @@
   }
 
   /// Implementation of [Uri.http].
-  factory _Uri.http(String authority, String unencodedPath,
-      [Map<String, dynamic>? queryParameters]) {
+  factory _Uri.http(String authority,
+      [String unencodedPath = '', Map<String, dynamic>? queryParameters]) {
     return _makeHttpUri("http", authority, unencodedPath, queryParameters);
   }
 
   /// Implementation of [Uri.https].
-  factory _Uri.https(String authority, String unencodedPath,
-      [Map<String, dynamic>? queryParameters]) {
+  factory _Uri.https(String authority,
+      [String unencodedPath = '', Map<String, dynamic>? queryParameters]) {
     return _makeHttpUri("https", authority, unencodedPath, queryParameters);
   }
 
diff --git a/tests/corelib/uri_http_test.dart b/tests/corelib/uri_http_test.dart
index bfc57bf..5cd8ccb 100644
--- a/tests/corelib/uri_http_test.dart
+++ b/tests/corelib/uri_http_test.dart
@@ -10,10 +10,13 @@
   }
 
   check(new Uri.http("", ""), "http:");
+  check(new Uri.http(""), "http:");
   check(new Uri.http("@:", ""), "http://");
+  check(new Uri.http("@:"), "http://");
   check(new Uri.http("@:8080", ""), "http://:8080");
+  check(new Uri.http("@:8080"), "http://:8080");
   check(new Uri.http("@host:", ""), "http://host");
-  check(new Uri.http("@host:", ""), "http://host");
+  check(new Uri.http("@host:"), "http://host");
   check(new Uri.http("xxx:yyy@host:8080", ""), "http://xxx:yyy@host:8080");
   check(new Uri.http("host", "a"), "http://host/a");
   check(new Uri.http("host", "/a"), "http://host/a");
@@ -46,10 +49,13 @@
   }
 
   check(new Uri.https("", ""), "https:");
+  check(new Uri.https(""), "https:");
   check(new Uri.https("@:", ""), "https://");
+  check(new Uri.https("@:"), "https://");
   check(new Uri.https("@:8080", ""), "https://:8080");
+  check(new Uri.https("@:8080"), "https://:8080");
   check(new Uri.https("@host:", ""), "https://host");
-  check(new Uri.https("@host:", ""), "https://host");
+  check(new Uri.https("@host:"), "https://host");
   check(new Uri.https("xxx:yyy@host:8080", ""), "https://xxx:yyy@host:8080");
   check(new Uri.https("host", "a"), "https://host/a");
   check(new Uri.https("host", "/a"), "https://host/a");
diff --git a/tests/corelib_2/uri_http_test.dart b/tests/corelib_2/uri_http_test.dart
index 9d02ebf..2441c28 100644
--- a/tests/corelib_2/uri_http_test.dart
+++ b/tests/corelib_2/uri_http_test.dart
@@ -12,10 +12,13 @@
   }
 
   check(new Uri.http("", ""), "http:");
+  check(new Uri.http(""), "http:");
   check(new Uri.http("@:", ""), "http://");
+  check(new Uri.http("@:"), "http://");
   check(new Uri.http("@:8080", ""), "http://:8080");
+  check(new Uri.http("@:8080"), "http://:8080");
   check(new Uri.http("@host:", ""), "http://host");
-  check(new Uri.http("@host:", ""), "http://host");
+  check(new Uri.http("@host:"), "http://host");
   check(new Uri.http("xxx:yyy@host:8080", ""), "http://xxx:yyy@host:8080");
   check(new Uri.http("host", "a"), "http://host/a");
   check(new Uri.http("host", "/a"), "http://host/a");
diff --git a/tests/lib/js/static_class_member_static_test.dart b/tests/lib/js/static_class_member_static_test.dart
new file mode 100644
index 0000000..cbaa314
--- /dev/null
+++ b/tests/lib/js/static_class_member_static_test.dart
@@ -0,0 +1,15 @@
+// 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.
+
+import 'package:js/js.dart';
+
+@JS('a.Foo')
+class Foo {
+  @JS('c.d.plus')
+  external static plus1(arg);
+//                ^
+// [web] JS interop static class members cannot have '.' in their JS name.
+}
+
+main() {}
diff --git a/tests/web/regress/issue/49129_test.dart b/tests/web/regress/issue/49129_test.dart
new file mode 100644
index 0000000..bb7b822
--- /dev/null
+++ b/tests/web/regress/issue/49129_test.dart
@@ -0,0 +1,39 @@
+// 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 is a regression test for a bug in how tear-offs of JSinterop top-level
+/// methods were generated.
+///
+/// A `.` in the JS-name of a interop method means that the method is accessible
+/// via a path of selectors, not that the `.` was meant to be part of the
+/// selector name.
+///
+/// See: https://github.com/dart-lang/sdk/issues/49129
+
+import 'package:js/js.dart';
+import 'package:expect/expect.dart';
+
+@JS('a.Foo.c.d.plus')
+external plus1(arg1);
+
+@JS('a.Foo.c.d')
+class Foo {
+  // Note: dots are not allowed in static class members, so the issue didn't
+  // arise in this case.
+  @JS('plus')
+  external static plus1(arg);
+}
+
+@JS()
+external eval(String s);
+
+main() {
+  eval('self.a = {Foo: {c: {d: {plus: function(a, b) { return a + 1; }}}}};');
+
+  // Tear-off for top-level with dotted names.
+  Expect.equals(2, (plus1)(1));
+
+  // Can also be accessed as a static method (with no dots).
+  Expect.equals(3, (Foo.plus1)(2));
+}
diff --git a/tests/web/web.status b/tests/web/web.status
index 9613b8b..091f925 100644
--- a/tests/web/web.status
+++ b/tests/web/web.status
@@ -41,6 +41,7 @@
 deferred_custom_loader_test: SkipByDesign # Issue 25683
 deferred_fail_and_retry_test: SkipByDesign # Uses eval to simulate failed loading.
 internal/object_members_test: SkipByDesign # Uses eval for interop
+regress/issue/49129_test: SkipByDesign # Uses eval for interop
 
 [ $compiler == dart2js && !$host_checked ]
 dummy_compiler_test: Slow, Pass # Issue 32439. self-hosting doesn't work with CFE yet.
diff --git a/tools/VERSION b/tools/VERSION
index 93cc43c..47b9487 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 18
 PATCH 0
-PRERELEASE 169
+PRERELEASE 170
 PRERELEASE_PATCH 0
\ No newline at end of file