Reapply "[dart2js,rti] Add direct methods for `as JSObject`"

This reverts commit 43b5c1432e14bdca6bfd80ddd1bfa5539ee73906.

This time, fall back on code that works for mocks and fakes.

Issue: #60746

Change-Id: Iecb822486729f8b10377c94bf87d06406bb0f5bc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/434920
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
diff --git a/pkg/compiler/lib/src/common/elements.dart b/pkg/compiler/lib/src/common/elements.dart
index 302cde2..1dc11ad 100644
--- a/pkg/compiler/lib/src/common/elements.dart
+++ b/pkg/compiler/lib/src/common/elements.dart
@@ -700,6 +700,9 @@
 
   late final ClassEntity jsUInt31Class = _findInterceptorsClass('JSUInt31');
 
+  // Static interop JavaScript object interface class.
+  late final ClassEntity jsObjectClass = _findInterceptorsClass('JSObject');
+
   /// Returns `true` member is the 'findIndexForNativeSubclassType' method
   /// declared in `dart:_interceptors`.
   bool isFindIndexForNativeSubclassType(MemberEntity member) {
@@ -1100,6 +1103,11 @@
   FunctionEntity get specializedAsStringNullable =>
       _findRtiFunction('_asStringQ');
 
+  FunctionEntity get specializedAsJSObject => _findRtiFunction('_asJSObject');
+
+  FunctionEntity get specializedAsJSObjectNullable =>
+      _findRtiFunction('_asJSObjectQ');
+
   FunctionEntity get instantiatedGenericFunctionType =>
       _findRtiFunction('instantiatedGenericFunctionType');
 
diff --git a/pkg/compiler/lib/src/js_backend/specialized_checks.dart b/pkg/compiler/lib/src/js_backend/specialized_checks.dart
index 7d7d4eb..47b4555 100644
--- a/pkg/compiler/lib/src/js_backend/specialized_checks.dart
+++ b/pkg/compiler/lib/src/js_backend/specialized_checks.dart
@@ -192,6 +192,15 @@
       return commonElements.specializedAsInt;
     }
 
+    if (element == commonElements.objectClass) {
+      if (!nullable) return commonElements.specializedAsObject;
+    }
+
+    if (element == commonElements.jsObjectClass) {
+      if (nullable) return commonElements.specializedAsJSObjectNullable;
+      return commonElements.specializedAsJSObject;
+    }
+
     return null;
   }
 }
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
index 12c0ade..ba9198b 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
@@ -1973,6 +1973,7 @@
       commonElements.listClass,
       commonElements.objectClass,
       commonElements.mapClass,
+      commonElements.jsObjectClass,
     ];
     // TODO(floitsch): this should probably be on a per-fragment basis.
 
diff --git a/sdk/lib/_internal/js_shared/lib/rti.dart b/sdk/lib/_internal/js_shared/lib/rti.dart
index 5df35b6..db73e91 100644
--- a/sdk/lib/_internal/js_shared/lib/rti.dart
+++ b/sdk/lib/_internal/js_shared/lib/rti.dart
@@ -1337,6 +1337,8 @@
       asFn = RAW_DART_FUNCTION_REF(_asNumQ);
     } else if (_Utils.isIdentical(testRti, TYPE_REF<double?>())) {
       asFn = RAW_DART_FUNCTION_REF(_asDoubleQ);
+    } else if (_Utils.isIdentical(testRti, TYPE_REF<JSObject?>())) {
+      asFn = RAW_DART_FUNCTION_REF(_asJSObjectQ);
     }
   } else {
     if (_Utils.isIdentical(testRti, TYPE_REF<int>())) {
@@ -1349,6 +1351,8 @@
       asFn = RAW_DART_FUNCTION_REF(_asNum);
     } else if (_Utils.isIdentical(testRti, TYPE_REF<double>())) {
       asFn = RAW_DART_FUNCTION_REF(_asDouble);
+    } else if (_Utils.isIdentical(testRti, TYPE_REF<JSObject>())) {
+      asFn = RAW_DART_FUNCTION_REF(_asJSObject);
     }
   }
   return asFn;
@@ -1460,6 +1464,27 @@
   return false;
 }
 
+/// A version of _isJSObject that does not need to be attached to an Rti `_is`
+/// method. The Rti is needed on the uncommon slow path (Dart mocks and fakes of
+/// interp objects), so this method falls back to using a full `is JSObject`
+/// expression, with some duplicated work on the uncommon paths.
+bool _isJSObjectStandalone(Object? object) {
+  // Keep the control flow here matching `_isJSObject`.
+  if (JS('bool', 'typeof # == "object"', object)) {
+    if (_isDartObject(object)) {
+      return object is JSObject;
+    }
+    return true;
+  }
+  if (JS('bool', 'typeof # == "function"', object)) {
+    if (JS_GET_FLAG('DEV_COMPILER')) {
+      return object is JSObject;
+    }
+    return true;
+  }
+  return false;
+}
+
 /// General unspecialized 'as' check that works for any type.
 /// Called from generated code.
 @pragma('dart2js:stack-starts-at-throw')
@@ -1701,6 +1726,23 @@
   throw _TypeError.forType(object, 'String?');
 }
 
+/// Specialization for 'as JSObject'.
+/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
+JSObject _asJSObject(Object? object) {
+  if (_isJSObjectStandalone(object)) return _Utils.asJSObject(object);
+  throw _TypeError.forType(object, 'JSObject');
+}
+
+/// Specialization for 'as JSObject?'.
+/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
+JSObject? _asJSObjectQ(Object? object) {
+  if (object == null) return _Utils.asNull(object);
+  if (_isJSObjectStandalone(object)) return _Utils.asJSObject(object);
+  throw _TypeError.forType(object, 'JSObject?');
+}
+
 String _rtiArrayToString(Object? array, List<String>? genericContext) {
   String s = '', sep = '';
   for (int i = 0; i < _Utils.arrayLength(array); i++) {
@@ -3920,6 +3962,7 @@
   static Rti asRti(Object? s) => JS('Rti', '#', s);
   static Rti? asRtiOrNull(Object? s) => JS('Rti|Null', '#', s);
   static _Type as_Type(Object? o) => JS('_Type', '#', o);
+  static JSObject asJSObject(Object? o) => JS('', '#', o);
 
   static bool isString(Object? o) => JS('bool', 'typeof # == "string"', o);
   static bool isNum(Object? o) => JS('bool', 'typeof # == "number"', o);
diff --git a/tests/web/internal/html_mocks_with_static_interop_test.dart b/tests/web/internal/html_mocks_with_static_interop_test.dart
index 3ad5704..fe51cb7 100644
--- a/tests/web/internal/html_mocks_with_static_interop_test.dart
+++ b/tests/web/internal/html_mocks_with_static_interop_test.dart
@@ -23,11 +23,14 @@
 }
 
 void main() {
-  test(MockWindow(), true);
-  test(window, false);
+  testIs(MockWindow(), true);
+  testIs(window, false);
+
+  testAs(MockWindow(), true);
+  testAs(window, false);
 }
 
-void test(Window w, bool isMock) {
+void testIs(Window w, bool isMock) {
   Expect.type<Window>(w);
   Expect.type<dartJsInterop.JSObject>(w);
 
@@ -46,3 +49,31 @@
     Expect.isFalse(doc is MockDocument);
   }
 }
+
+void testAs(Window w, bool isMock) {
+  asCheck<Window>(w);
+  asCheck<dartJsInterop.JSObject>(w);
+
+  final doc = w.document;
+
+  asCheck<HtmlDocument>(doc);
+  asCheck<dartJsInterop.JSObject>(doc);
+
+  if (isMock) {
+    asCheck<MockWindow>(w);
+    asCheck<MockDocument>(doc);
+    w as MockWindow;
+    doc as MockDocument;
+  } else {
+    Expect.throws(() => asCheck<MockWindow>(w));
+    Expect.throws(() => asCheck<MockDocument>(doc));
+    Expect.throws(() => w as MockWindow);
+    Expect.throws(() => doc as MockDocument);
+  }
+
+  asCheck<String>('hello');
+  Expect.throws(() => asCheck<String>(w));
+}
+
+@pragma('dart2js:never-inline')
+void asCheck<T>(Object? o) => o as T;