[linter/analyzer] Add lint for runtime type tests of JS types

Closes https://github.com/dart-lang/linter/issues/4841

Adds a lint to verify and report whether an is check or a cast
may result in platform-inconsistent behavior. In order to do so,
adds necessary additions to canBeSubtypeOf and extends the mock
SDK. Also adds this to the list of default rules.

Change-Id: I7f235ee698419d9f253cc96f08322bedca271825
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/361722
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
index cc91a6c..902f6bd 100644
--- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
+++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
@@ -2128,6 +2128,12 @@
   status: hasFix
 LintCode.invalid_case_patterns:
   status: hasFix
+LintCode.invalid_runtime_check_with_js_interop_types:
+  status: needsFix
+  notes: |-
+    The fix is dependent on the specific types that are being used. In some
+    cases, the type test is incorrect and has no fix besides removing the test.
+    In other cases, JS interop helpers may be used to do the type test instead.
 LintCode.invariant_booleans:
   status: noFix
   notes: |-
diff --git a/pkg/analysis_server/test/services/completion/dart/completion_test.dart b/pkg/analysis_server/test/services/completion/dart/completion_test.dart
index f81d097..115fdd3 100644
--- a/pkg/analysis_server/test/services/completion/dart/completion_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/completion_test.dart
@@ -2498,6 +2498,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -8906,6 +8910,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -8969,6 +8977,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -9015,6 +9027,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   my_lib.dart
diff --git a/pkg/analysis_server/test/services/completion/dart/declaration/imported_reference_test.dart b/pkg/analysis_server/test/services/completion/dart/declaration/imported_reference_test.dart
index dd4a7b6..ba7e699 100644
--- a/pkg/analysis_server/test/services/completion/dart/declaration/imported_reference_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/declaration/imported_reference_test.dart
@@ -3564,6 +3564,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
diff --git a/pkg/analysis_server/test/services/completion/dart/declaration/local_reference_test.dart b/pkg/analysis_server/test/services/completion/dart/declaration/local_reference_test.dart
index fed91b4..ea8e3a2 100644
--- a/pkg/analysis_server/test/services/completion/dart/declaration/local_reference_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/declaration/local_reference_test.dart
@@ -4105,6 +4105,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
diff --git a/pkg/analysis_server/test/services/completion/dart/declaration/type_member_test.dart b/pkg/analysis_server/test/services/completion/dart/declaration/type_member_test.dart
index 4d37608..37b913f 100644
--- a/pkg/analysis_server/test/services/completion/dart/declaration/type_member_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/declaration/type_member_test.dart
@@ -2616,6 +2616,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
diff --git a/pkg/analysis_server/test/services/completion/dart/declaration/uri_test.dart b/pkg/analysis_server/test/services/completion/dart/declaration/uri_test.dart
index 7b4d37d..f44c198 100644
--- a/pkg/analysis_server/test/services/completion/dart/declaration/uri_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/declaration/uri_test.dart
@@ -127,6 +127,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -166,6 +170,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -207,6 +215,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -246,6 +258,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -285,6 +301,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -326,6 +346,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -380,6 +404,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -421,6 +449,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -456,6 +488,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -494,6 +530,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -840,6 +880,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -897,6 +941,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -938,6 +986,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
@@ -992,6 +1044,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
diff --git a/pkg/analysis_server/test/services/completion/dart/location/import_directive_test.dart b/pkg/analysis_server/test/services/completion/dart/location/import_directive_test.dart
index 25c5414..c106f85 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/import_directive_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/import_directive_test.dart
@@ -125,6 +125,10 @@
     kind: import
   dart:isolate
     kind: import
+  dart:js
+    kind: import
+  dart:js_interop
+    kind: import
   dart:math
     kind: import
   dart:typed_data
diff --git a/pkg/analyzer/lib/src/dart/element/type_system.dart b/pkg/analyzer/lib/src/dart/element/type_system.dart
index 17cdf44..d6fb5af 100644
--- a/pkg/analyzer/lib/src/dart/element/type_system.dart
+++ b/pkg/analyzer/lib/src/dart/element/type_system.dart
@@ -138,9 +138,15 @@
   /// not a subtype of `int`. More generally, we check if there could be a
   /// type that implements both [left] and [right], regardless of whether
   /// [left] is a subtype of [right], or [right] is a subtype of [left].
-  bool canBeSubtypeOf(DartType left, DartType right) {
-    left = left.extensionTypeErasure;
-    right = right.extensionTypeErasure;
+  ///
+  /// If [eraseTypes] is not null, this function uses that function to erase the
+  /// extension types within [left] and [right]. Otherwise, it uses the
+  /// extension type erasure.
+  bool canBeSubtypeOf(DartType left, DartType right,
+      {(DartType, DartType) Function(DartType, DartType)? eraseTypes}) {
+    (left, right) = eraseTypes != null
+        ? eraseTypes(left, right)
+        : (left.extensionTypeErasure, right.extensionTypeErasure);
 
     // If one is `Null`, then the other must be nullable.
     var leftIsNullable = isPotentiallyNullable(left);
@@ -173,7 +179,8 @@
     if (left.isDartAsyncFutureOr) {
       var base = futureOrBase(left);
       var future = typeProvider.futureType(base);
-      return canBeSubtypeOf(base, right) || canBeSubtypeOf(future, right);
+      return canBeSubtypeOf(base, right, eraseTypes: eraseTypes) ||
+          canBeSubtypeOf(future, right, eraseTypes: eraseTypes);
     }
 
     // FutureOr<T> = T || Future<T>
@@ -181,7 +188,8 @@
     if (right.isDartAsyncFutureOr) {
       var base = futureOrBase(right);
       var future = typeProvider.futureType(base);
-      return canBeSubtypeOf(left, base) || canBeSubtypeOf(left, future);
+      return canBeSubtypeOf(left, base, eraseTypes: eraseTypes) ||
+          canBeSubtypeOf(left, future, eraseTypes: eraseTypes);
     }
 
     if (left is InterfaceTypeImpl && right is InterfaceTypeImpl) {
@@ -200,7 +208,8 @@
         var rightArguments = right.typeArguments;
         assert(leftArguments.length == rightArguments.length);
         for (var i = 0; i < leftArguments.length; i++) {
-          if (!canBeSubtypeOf(leftArguments[i], rightArguments[i])) {
+          if (!canBeSubtypeOf(leftArguments[i], rightArguments[i],
+              eraseTypes: eraseTypes)) {
             return false;
           }
         }
@@ -280,7 +289,8 @@
       for (var i = 0; i < left.positionalFields.length; i++) {
         var leftField = left.positionalFields[i];
         var rightField = right.positionalFields[i];
-        if (!canBeSubtypeOf(leftField.type, rightField.type)) {
+        if (!canBeSubtypeOf(leftField.type, rightField.type,
+            eraseTypes: eraseTypes)) {
           return false;
         }
       }
@@ -294,7 +304,8 @@
         if (leftField.name != rightField.name) {
           return false;
         }
-        if (!canBeSubtypeOf(leftField.type, rightField.type)) {
+        if (!canBeSubtypeOf(leftField.type, rightField.type,
+            eraseTypes: eraseTypes)) {
           return false;
         }
       }
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_packages.dart b/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
index 5153a48..f248814 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
@@ -59,9 +59,8 @@
     libFolder.getChildAssumingFile('js.dart').writeAsStringSync(r'''
 library js;
 
-class JS {
-  const JS([String js]);
-}
+// ignore: EXPORT_INTERNAL_LIBRARY
+export 'dart:_js_annotations' show JS, staticInterop;
 ''');
   }
 
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
index 4d9ba71..0697003 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
@@ -1387,6 +1387,86 @@
   )
 ]);
 
+final MockSdkLibrary _LIB_JS = MockSdkLibrary('js', [
+  MockSdkLibraryUnit(
+    'js/js.dart',
+    '''
+library dart.js;
+
+class JsObject {}
+''',
+  )
+]);
+
+final MockSdkLibrary _LIB_JS_ANNOTATIONS = MockSdkLibrary('_js_annotations', [
+  MockSdkLibraryUnit(
+    'js/_js_annotations.dart',
+    '''
+library _js_annotations;
+
+class JS {
+  final String? name;
+  const JS([this.name]);
+}
+
+class _StaticInterop {
+  const _StaticInterop();
+}
+
+const _StaticInterop staticInterop = _StaticInterop();
+''',
+  )
+]);
+
+final MockSdkLibrary _LIB_JS_INTEROP = MockSdkLibrary(
+  'js_interop',
+  [
+    MockSdkLibraryUnit(
+      'js/js_interop.dart',
+      '''
+library;
+
+import 'dart:typed_data';
+
+export 'dart:_js_annotations' show staticInterop;
+
+class JS {
+  final String? name;
+  const JS([this.name]);
+}
+
+extension type JSAny._(Object _) implements Object {}
+
+extension type JSBoolean._(bool _) implements JSAny {}
+
+extension type JSString._(String _) implements JSAny {}
+
+extension type JSNumber._(num _) implements JSAny {}
+
+extension type JSObject._(Object _) implements JSAny {}
+
+extension type JSArray<T extends JSAny?>._(List _) implements JSObject {}
+
+extension type JSTypedArray._(TypedData _) implements JSObject {}
+
+extension type JSUint8List._(Uint8List _) implements JSTypedArray {}
+''',
+    )
+  ],
+);
+
+final MockSdkLibrary _LIB_MACROS = MockSdkLibrary(
+  '_macros',
+  [
+    MockSdkLibraryUnit(
+      '_macros/_macros.dart',
+      '''
+library dart._macros;
+''',
+    )
+  ],
+);
+
 final MockSdkLibrary _LIB_MATH = MockSdkLibrary(
   'math',
   [
@@ -1463,6 +1543,10 @@
   _LIB_INTERNAL,
   _LIB_IO,
   _LIB_ISOLATE,
+  _LIB_JS,
+  _LIB_JS_ANNOTATIONS,
+  _LIB_JS_INTEROP,
+  _LIB_MACROS,
   _LIB_MATH,
   _LIB_TYPED_DATA,
   _LIB_WASM,
diff --git a/pkg/linter/CHANGELOG.md b/pkg/linter/CHANGELOG.md
index f5f3b82..12cb41a 100644
--- a/pkg/linter/CHANGELOG.md
+++ b/pkg/linter/CHANGELOG.md
@@ -25,6 +25,7 @@
 
 - new lint: `unnecessary_library_name`
 - new lint: `missing_code_block_language_in_doc_comment`
+- new lint: `invalid_runtime_check_with_js_interop_types`
 
 # 3.3.0
 
diff --git a/pkg/linter/example/all.yaml b/pkg/linter/example/all.yaml
index c567f4f..32b93fc 100644
--- a/pkg/linter/example/all.yaml
+++ b/pkg/linter/example/all.yaml
@@ -81,6 +81,7 @@
     - implicit_call_tearoffs
     - implicit_reopen
     - invalid_case_patterns
+    - invalid_runtime_check_with_js_interop_types
     - join_return_with_assignment
     - leading_newlines_in_multiline_strings
     - library_annotations
diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart
index ceaadb7..e1f77a2 100644
--- a/pkg/linter/lib/src/rules.dart
+++ b/pkg/linter/lib/src/rules.dart
@@ -88,6 +88,7 @@
 import 'rules/implicit_call_tearoffs.dart';
 import 'rules/implicit_reopen.dart';
 import 'rules/invalid_case_patterns.dart';
+import 'rules/invalid_runtime_check_with_js_interop_types.dart';
 import 'rules/invariant_booleans.dart';
 import 'rules/iterable_contains_unrelated_type.dart';
 import 'rules/join_return_with_assignment.dart';
@@ -329,6 +330,7 @@
     ..register(InvariantBooleans())
     ..register(IterableContainsUnrelatedType())
     ..register(JoinReturnWithAssignment())
+    ..register(InvalidRuntimeCheckWithJSInteropTypes())
     ..register(LeadingNewlinesInMultilineStrings())
     ..register(LibraryAnnotations())
     ..register(LibraryNames())
diff --git a/pkg/linter/lib/src/rules/invalid_runtime_check_with_js_interop_types.dart b/pkg/linter/lib/src/rules/invalid_runtime_check_with_js_interop_types.dart
new file mode 100644
index 0000000..44fdca3
--- /dev/null
+++ b/pkg/linter/lib/src/rules/invalid_runtime_check_with_js_interop_types.dart
@@ -0,0 +1,439 @@
+// Copyright (c) 2024, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/dart/element/type_provider.dart';
+// ignore: implementation_imports
+import 'package:analyzer/src/dart/element/type.dart';
+// ignore: implementation_imports
+import 'package:analyzer/src/dart/element/type_system.dart'
+    show TypeSystemImpl, ExtensionTypeErasure;
+
+import '../analyzer.dart';
+
+const String _dartJsAnnotationsUri = 'dart:_js_annotations';
+const String _dartJsInteropUri = 'dart:js_interop';
+const String _dartJsUri = 'dart:js';
+
+const _desc =
+    r'''Avoid runtime type tests with JS interop types where the result may not
+    be platform-consistent.''';
+
+const _details = r'''
+**DON'T** use 'is' checks where the type is a JS interop type.
+
+**DON'T** use 'is' checks where the type is a generic Dart type that has JS
+interop type arguments.
+
+**DON'T** use 'is' checks with a JS interop value.
+
+'dart:js_interop' types have runtime types that are different based on whether
+you are compiling to JS or to Wasm. Therefore, runtime type checks may result in
+different behavior. Runtime checks also do not necessarily check that a JS
+interop value is a particular JavaScript type.
+
+**BAD:**
+```dart
+extension type Window(JSObject o) {}
+
+void compute(JSAny a, bool b, List<JSObject> lo, List<String> ls, JSObject o) {
+  a is String; // LINT, checking that a JS value is a Dart type
+  b is JSBoolean; // LINT, checking that a Dart value is a JS type
+  a is JSString; // LINT, checking that a JS value is a different JS interop
+                 // type
+  o is JSNumber; // LINT, checking that a JS value is a different JS interop
+                 // type
+  lo is List<String>; // LINT, JS interop type argument and Dart type argument
+                      // are incompatible
+  ls is List<JSString>; // LINT, Dart type argument and JS interop type argument
+                        // are incompatible
+  lo is List<JSArray>; // LINT, comparing JS interop type argument with
+                       // different JS interop type argument
+  lo is List<JSNumber>; // LINT, comparing JS interop type argument with
+                        // different JS interop type argument
+  // Not a lint, but this doesn't actually check whether `o` is actually a
+  // Window.
+  o is Window;
+}
+```
+
+Prefer using JS interop helpers like 'isA' from 'dart:js_interop' to check the
+type of JS interop values.
+
+**GOOD:**
+```dart
+extension type Window(JSObject o) {}
+
+void compute(JSAny a, List<JSAny> l, JSObject o) {
+  a.isA<JSString>; // OK, uses JS interop to check it is a JS string
+  l[0].isA<JSString>; // OK, uses JS interop to check it is a JS string
+  o.isA<Window>(); // OK, uses JS interop to check `o` is a Window
+}
+```
+
+**DON'T** use 'as' to cast a JS interop value to an unrelated Dart type or an
+unrelated Dart value to a JS interop type.
+
+**DON'T** use 'as' to cast a JS interop value to a JS interop type represented
+by an incompatible 'dart:js_interop' type.
+
+**BAD:**
+```dart
+extension type Window(JSObject o) {}
+
+void compute(String s, JSBoolean b, Window w, List<String> l,
+    List<JSObject> lo) {
+  s as JSString; // LINT, casting Dart type to JS interop type
+  b as bool; // LINT, casting JS interop type to Dart type
+  b as JSNumber; // LINT, JSBoolean and JSNumber are incompatible
+  b as Window; // LINT, JSBoolean and JSObject are incompatible
+  w as JSBoolean; // LINT, JSObject and JSBoolean are incompatible
+  l as List<JSString>; // LINT, casting Dart value with Dart type argument to
+                       // Dart type with JS interop type argument
+  lo as List<String>; // LINT, casting Dart value with JS interop type argument
+                      // to Dart type with Dart type argument
+  lo as List<JSBoolean>; // LINT, casting Dart value with JS interop type
+                         // argument to Dart type with incompatible JS interop
+                         // type argument
+}
+```
+
+Prefer using 'dart:js_interop' conversion methods to convert a JS interop value
+to a Dart value and vice versa.
+
+**GOOD:**
+```dart
+extension type Window(JSObject o) {}
+extension type Document(JSObject o) {}
+
+void compute(String s, JSBoolean b, Window w, JSArray<JSString> a,
+    List<String> ls, JSObject o, List<JSAny> la) {
+  s.toJS; // OK, converts the Dart type to a JS type
+  b.toDart; // OK, converts the JS type to a Dart type
+  a.toDart; // OK, converts the JS type to a Dart type
+  w as Document; // OK, but no runtime check that `w` is a JS Document
+  ls.map((e) => e.toJS).toList(); // OK, converts the Dart types to JS types
+  o as JSArray<JSString>; // OK, JSObject and JSArray are compatible
+  la as List<JSString>; // OK, JSAny and JSString are compatible
+  (o as Object) as JSObject; // OK, Object is a supertype of JSAny
+}
+```
+
+''';
+const Set<String> _sdkWebLibraries = {
+  'dart:html',
+  'dart:indexed_db',
+  'dart:svg',
+  'dart:web_audio',
+  'dart:web_gl',
+};
+
+/// If [type] has a `dart:js_interop` type equivalent, return that equivalent
+/// type.
+///
+/// The only such type that satisfies this is `@staticInterop` types that were
+/// declared using the `@JS` annotation in `dart:js_interop`. These types are
+/// always equal to `JSObject`. `@staticInterop` types that were declared using
+/// `package:js` do not apply as that package is incompatible with dart2wasm.
+DartType? getDartJsInteropEquivalent(InterfaceType type) {
+  var element = type.element;
+  if (element is! ClassElement) return null;
+  var metadata = element.metadata;
+  var hasJS = false;
+  var hasStaticInterop = false;
+  late LibraryElement dartJsInterop;
+  for (var i = 0; i < metadata.length; i++) {
+    var annotation = metadata[i];
+    var annotationElement = annotation.element;
+    if (annotationElement is ConstructorElement &&
+        isFromLibrary(annotationElement.library, _dartJsInteropUri) &&
+        annotationElement.enclosingElement.name == 'JS') {
+      hasJS = true;
+      dartJsInterop = annotationElement.library;
+    } else if (annotationElement is PropertyAccessorElement &&
+        isFromLibrary(annotationElement.library, _dartJsAnnotationsUri) &&
+        annotationElement.name == 'staticInterop') {
+      hasStaticInterop = true;
+    }
+  }
+  return (hasJS && hasStaticInterop)
+      ? dartJsInterop.units.single.extensionTypes
+          .singleWhere((extType) => extType.name == 'JSObject')
+          // Nullability is ignored in this lint, so just return `thisType`.
+          .thisType
+      : null;
+}
+
+/// Given a [type] erased by [EraseNonJSInteropTypes], determine if it is a type
+/// that is a `dart:js_interop` type or is bound to one.
+bool isDartJsInteropType(DartType type) {
+  if (type is TypeParameterType) return isDartJsInteropType(type.bound);
+  if (type is InterfaceType) {
+    var element = type.element;
+    if (element is ExtensionTypeElement) {
+      return isFromLibrary(element.library, _dartJsInteropUri);
+    }
+  }
+  return false;
+}
+
+bool isFromLibrary(LibraryElement elementLibrary, String uri) =>
+    elementLibrary.definingCompilationUnit.source ==
+    elementLibrary.context.sourceFactory.forUri(uri);
+
+/// Whether [type] comes from a JS interop library that is unavailable in
+/// dart2wasm.
+///
+/// Currently, the set of interop libraries that declare types or allow users to
+/// declare types includes `package:js`, the SDK web libraries, and `dart:js`.
+///
+/// Since dart2wasm doesn't support these libraries, users can't come across
+/// platform-inconsistent type tests, and therefore we should not lint for these
+/// types.
+bool isWasmIncompatibleJsInterop(DartType type) {
+  if (type is TypeParameterType) return isWasmIncompatibleJsInterop(type.bound);
+  if (type is! InterfaceType) return false;
+  var element = type.element;
+  // `hasJS` only checks for the `dart:_js_annotations` definition, which is
+  // what we want here.
+  if (element.hasJS) return true;
+  return _sdkWebLibraries.any((uri) => isFromLibrary(element.library, uri)) ||
+      // While a type test with types from this library is very rare, we should
+      // still ignore it for consistency.
+      isFromLibrary(element.library, _dartJsUri);
+}
+
+/// Erases extension types except for `dart:js_interop` interop types so that
+/// [_Visitor.getInvalidJsInteropTypeTest] can determine if the type test is safe.
+class EraseNonJSInteropTypes extends ExtensionTypeErasure {
+  const EraseNonJSInteropTypes();
+
+  @override
+  DartType? visitInterfaceType(covariant InterfaceTypeImpl type) {
+    if (isDartJsInteropType(type)) {
+      // Generics on `dart:js_interop` types are not forwarded to the underlying
+      // representation type as we can't guarantee the container actually
+      // contains that type. Therefore, any subtype check involving a generic
+      // `dart:js_interop` type should ignore the type parameters. Nullability
+      // is also ignored for the purpose of this lint, so it suffices to just
+      // return the `thisType` of the `dart:js_interop` type.
+      return type.typeArguments.isNotEmpty ? type.element.thisType : type;
+    } else {
+      return getDartJsInteropEquivalent(type) ?? super.visitInterfaceType(type);
+    }
+  }
+
+  @override
+  DartType? visitTypeParameterType(TypeParameterType type) {
+    // If the bound is a JS interop type, replace it with its `dart:js_interop`
+    // equivalent.
+    var newBound = type.bound.accept(this);
+    return createPromotedTypeParameterType(
+      type: type,
+      newNullability: type.nullabilitySuffix,
+      newPromotedBound: newBound,
+    );
+  }
+}
+
+class InvalidRuntimeCheckWithJSInteropTypes extends LintRule {
+  static const String lintName = 'invalid_runtime_check_with_js_interop_types';
+
+  static const LintCode dartTypeIsJsInteropTypeCode = LintCode(
+      lintName,
+      "Runtime check between '{0}' and '{1}' checks whether a Dart value is a "
+      'JS interop type, which may not be platform-consistent.');
+
+  static const LintCode jsInteropTypeIsDartTypeCode = LintCode(
+      lintName,
+      "Runtime check between '{0}' and '{1}' checks whether a JS interop value "
+      'is a Dart type, which may not be platform-consistent.');
+
+  static const LintCode jsInteropTypeIsJsInteropTypeCode = LintCode(
+      lintName,
+      "Runtime check between '{0}' and '{1}' involves a non-trivial runtime "
+      'check between two JS interop types, which may not be '
+      'platform-consistent.',
+      correctionMessage:
+          "Try using a JS interop member like 'isA' from 'dart:js_interop' to "
+          'check the type of JS interop values.');
+
+  static const LintCode dartTypeAsJsInteropTypeCode = LintCode(
+      lintName,
+      "Cast from '{0}' to '{1}' casts a Dart value to a JS interop type, which "
+      'may not be platform-consistent.',
+      correctionMessage:
+          "Try using conversion methods from 'dart:js_interop' to convert "
+          'between Dart types and JS interop types.');
+
+  static const LintCode jsInteropTypeAsDartTypeCode = LintCode(
+      lintName,
+      "Cast from '{0}' to '{1}' casts a JS interop value to a Dart type, which "
+      'may not be platform-consistent.',
+      correctionMessage:
+          "Try using conversion methods from 'dart:js_interop' to convert "
+          'between JS interop types and Dart types.');
+
+  static const LintCode jsInteropTypeAsIncompatibleJsInteropTypeCode = LintCode(
+      lintName,
+      "Cast from '{0}' to '{1}' casts a JS interop value to an incompatible JS "
+      'interop type, which may not be platform-consistent.');
+
+  InvalidRuntimeCheckWithJSInteropTypes()
+      : super(
+            name: lintName,
+            description: _desc,
+            details: _details,
+            group: Group.errors);
+
+  @override
+  void registerNodeProcessors(
+      NodeLintRegistry registry, LinterContext context) {
+    var visitor = _Visitor(this, context.typeSystem, context.typeProvider);
+    registry.addIsExpression(this, visitor);
+    registry.addAsExpression(this, visitor);
+  }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+  final TypeSystemImpl typeSystem;
+  final TypeProvider typeProvider;
+  late final EraseNonJSInteropTypes _eraseNonJsInteropTypes;
+
+  _Visitor(this.rule, TypeSystem typeSystem, this.typeProvider)
+      : typeSystem = typeSystem as TypeSystemImpl,
+        _eraseNonJsInteropTypes = EraseNonJSInteropTypes();
+
+  /// Determines if a type test from [leftType] to [rightType] is a valid test
+  /// for JS interop, and if not, returns the [LintCode] associated with the
+  /// specific invalid case.
+  ///
+  /// If [check] is true, this determines if it's a valid `is` check. Otherwise,
+  /// determines if it's a valid `as` cast.
+  ///
+  /// If neither type contains a JS interop type, it's a valid test and this
+  /// returns null.
+  ///
+  /// `dart:js_interop` types are represented by platform-dependent values and
+  /// therefore, type tests that aren't statically guaranteed may be
+  /// inconsistent. In order to determine if this test is valid, this function
+  /// erases any interop types to its `dart:js_interop` equivalent and then
+  /// compares. If there are nested types involved, it nests as needed.
+  ///
+  /// An `is` check with a `dart:js_interop` type is valid only if the
+  /// positionally-equivalent type in [leftType] <: the positionally-equivalent
+  /// type in [rightType] or if the type in [rightType] is `dynamic`. An `as`
+  /// cast is the same except it also allows the type in [leftType] to be a
+  /// supertype or `dynamic`.
+  ///
+  /// These restrictions come from the difference in the representation of
+  /// `dart:js_interop` types. When compiling to Wasm, JS values are not
+  /// differentiated at runtime, and therefore there's only one runtime type.
+  /// When compiling to JS, JS values are differentiated and each
+  /// `dart:js_interop` has a separate runtime type.
+  ///
+  /// Types that belong to JS interop libraries that are not available when
+  /// compiling to Wasm are ignored. Nullability is also ignored for the purpose
+  /// of this test.
+  LintCode? getInvalidJsInteropTypeTest(DartType? leftType, DartType? rightType,
+      {required bool check}) {
+    if (leftType == null || rightType == null) return null;
+    LintCode? lintCode;
+    (DartType, DartType) eraseTypes(DartType left, DartType right) {
+      DartType left_ =
+          typeSystem.promoteToNonNull(_eraseNonJsInteropTypes.perform(left));
+      DartType right_ =
+          typeSystem.promoteToNonNull(_eraseNonJsInteropTypes.perform(right));
+      var leftIsInteropType = isDartJsInteropType(left_);
+      var rightIsInteropType = isDartJsInteropType(right_);
+      // If there's already an invalid check in this `canBeSubtypeOf` check, we
+      // are already going to lint, so only continue checking if we haven't
+      // found an issue.
+      if (lintCode == null) {
+        if (leftIsInteropType || rightIsInteropType) {
+          if (!isWasmIncompatibleJsInterop(left_) &&
+              !isWasmIncompatibleJsInterop(right_)) {
+            var leftIsSubtype = typeSystem.isSubtypeOf(left_, right_);
+            var rightIsSubtype = typeSystem.isSubtypeOf(right_, left_);
+            var leftIsDynamic = left_ is DynamicType;
+            var rightIsDynamic = right_ is DynamicType;
+            if (check) {
+              if (!leftIsSubtype && !rightIsDynamic) {
+                if (leftIsInteropType && rightIsInteropType) {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .jsInteropTypeIsJsInteropTypeCode;
+                } else if (leftIsInteropType) {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .jsInteropTypeIsDartTypeCode;
+                } else {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .dartTypeIsJsInteropTypeCode;
+                }
+              }
+            } else {
+              if (!leftIsSubtype &&
+                  !rightIsSubtype &&
+                  !leftIsDynamic &&
+                  !rightIsDynamic) {
+                if (leftIsInteropType && rightIsInteropType) {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .jsInteropTypeAsIncompatibleJsInteropTypeCode;
+                } else if (leftIsInteropType) {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .jsInteropTypeAsDartTypeCode;
+                } else {
+                  lintCode = InvalidRuntimeCheckWithJSInteropTypes
+                      .dartTypeAsJsInteropTypeCode;
+                }
+              }
+            }
+          }
+        }
+      }
+      // The resulting types that are checked in `canBeSubtypeOf` are assumed to
+      // not be extension types, so erase the types if we avoided erasing them
+      // in `EraseNonJSInteropTypes` before continuing.
+      if (leftIsInteropType) left_ = left.extensionTypeErasure;
+      if (rightIsInteropType) right_ = right.extensionTypeErasure;
+      return (left_, right_);
+    }
+
+    typeSystem.canBeSubtypeOf(leftType, rightType, eraseTypes: eraseTypes);
+    return lintCode;
+  }
+
+  @override
+  void visitAsExpression(AsExpression node) {
+    var leftType = node.expression.staticType;
+    var rightType = node.type.type;
+    var code = getInvalidJsInteropTypeTest(leftType, rightType, check: false);
+    if (code != null) {
+      rule.reportLint(node,
+          arguments: [
+            leftType!.getDisplayString(),
+            rightType!.getDisplayString(),
+          ],
+          errorCode: code);
+    }
+  }
+
+  @override
+  void visitIsExpression(IsExpression node) {
+    var leftType = node.expression.staticType;
+    var rightType = node.type.type;
+    var code = getInvalidJsInteropTypeTest(leftType, rightType, check: true);
+    if (code != null) {
+      rule.reportLint(node,
+          arguments: [
+            leftType!.getDisplayString(),
+            rightType!.getDisplayString(),
+          ],
+          errorCode: code);
+    }
+  }
+}
diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart
index f5722e8..c57210c 100644
--- a/pkg/linter/test/rules/all.dart
+++ b/pkg/linter/test/rules/all.dart
@@ -101,6 +101,8 @@
 import 'hash_and_equals_test.dart' as hash_and_equals;
 import 'implicit_reopen_test.dart' as implicit_reopen;
 import 'invalid_case_patterns_test.dart' as invalid_case_patterns;
+import 'invalid_runtime_check_with_js_interop_types_test.dart'
+    as invalid_runtime_check_with_js_interop_types_test;
 import 'join_return_with_assignment_test.dart' as join_return_with_assignment;
 import 'leading_newlines_in_multiline_strings_test.dart'
     as leading_newlines_in_multiline_strings;
@@ -339,6 +341,7 @@
   hash_and_equals.main();
   implicit_reopen.main();
   invalid_case_patterns.main();
+  invalid_runtime_check_with_js_interop_types_test.main();
   join_return_with_assignment.main();
   leading_newlines_in_multiline_strings.main();
   library_annotations.main();
diff --git a/pkg/linter/test/rules/invalid_runtime_check_with_js_interop_types_test.dart b/pkg/linter/test/rules/invalid_runtime_check_with_js_interop_types_test.dart
new file mode 100644
index 0000000..f2c0225
--- /dev/null
+++ b/pkg/linter/test/rules/invalid_runtime_check_with_js_interop_types_test.dart
@@ -0,0 +1,937 @@
+// Copyright (c) 2024, 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:linter/src/rules/invalid_runtime_check_with_js_interop_types.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../rule_test_support.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(InvalidRuntimeCheckWithJSInteropTypesTest);
+  });
+}
+
+@reflectiveTest
+class InvalidRuntimeCheckWithJSInteropTypesTest extends LintRuleTest {
+  @override
+  bool get addJsPackageDep => true;
+
+  @override
+  String get lintRule => InvalidRuntimeCheckWithJSInteropTypes.lintName;
+
+  test_baseTypesAs_dart_type_as_js_type() async {
+    await _testCasts([_AsCast('int', 'JSNumber')]);
+  }
+
+  test_baseTypesAs_js_type_as_dart_type() async {
+    await _testCasts([_AsCast('JSString', 'String')]);
+  }
+
+  test_baseTypesAs_js_type_as_same_js_type() async {
+    await _testCasts(
+        [_AsCast('JSBoolean', 'JSBoolean', lint: false, unnecessary: true)]);
+  }
+
+  test_baseTypesAs_js_type_as_subtype() async {
+    await _testCasts([_AsCast('JSAny', 'JSObject', lint: false)]);
+  }
+
+  test_baseTypesAs_js_type_as_supertype() async {
+    await _testCasts([_AsCast('JSArray', 'JSObject', lint: false)]);
+  }
+
+  test_baseTypesAs_js_type_as_top_type() async {
+    await _testCasts([
+      _AsCast('JSAny', 'Object', lint: false),
+      _AsCast('JSObject', 'dynamic', lint: false),
+      _AsCast('List<JSAny>', 'Object', lint: false),
+      _AsCast('List<JSObject>', 'dynamic', lint: false)
+    ]);
+  }
+
+  test_baseTypesAs_js_type_as_unrelated_js_type() async {
+    await _testCasts([_AsCast('JSBoolean', 'JSUint8List')]);
+  }
+
+  test_baseTypesAs_top_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('Object', 'JSAny', lint: false),
+      _AsCast('dynamic', 'JSObject', lint: false),
+      _AsCast('List<JSAny>', 'Object', lint: false),
+      _AsCast('List<JSObject>', 'dynamic', lint: false)
+    ]);
+  }
+
+  test_baseTypesIs_dart_type_is_js_type() async {
+    await _testChecks([_IsCheck('int', 'JSNumber')]);
+  }
+
+  test_baseTypesIs_js_type_is_dart_type() async {
+    await _testChecks([_IsCheck('JSString', 'String')]);
+  }
+
+  test_baseTypesIs_js_type_is_same_js_type() async {
+    await _testChecks(
+        [_IsCheck('JSBoolean', 'JSBoolean', lint: false, unnecessary: true)]);
+  }
+
+  test_baseTypesIs_js_type_is_subtype() async {
+    await _testChecks([_IsCheck('JSAny', 'JSObject')]);
+  }
+
+  test_baseTypesIs_js_type_is_supertype() async {
+    await _testChecks(
+        [_IsCheck('JSArray', 'JSObject', lint: false, unnecessary: true)]);
+  }
+
+  test_baseTypesIs_js_type_is_top_type() async {
+    await _testChecks([
+      _IsCheck('JSAny', 'Object', lint: false, unnecessary: true),
+      _IsCheck('JSObject', 'dynamic', lint: false, unnecessary: true),
+      _IsCheck('List<JSAny>', 'Object', lint: false, unnecessary: true),
+      _IsCheck('List<JSObject>', 'dynamic', lint: false, unnecessary: true)
+    ]);
+  }
+
+  test_baseTypesIs_js_type_is_unrelated_js_type() async {
+    await _testChecks([_IsCheck('JSBoolean', 'JSUint8List')]);
+  }
+
+  test_baseTypesIs_top_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('Object', 'JSAny'),
+      _IsCheck('dynamic', 'JSObject'),
+      // TODO(srujzs): These two checks should lint, but `canBeSubtypeOf`
+      // determines subtyping via a pairwise comparison. So, we never actually
+      // check the JS interop type against anything, since the left type is not
+      // generic.
+      _IsCheck('Object', 'List<JSAny>', lint: false),
+      _IsCheck('dynamic', 'List<JSObject>', lint: false)
+    ]);
+  }
+
+  // TODO(srujzs): Some of the following type tests should result in an error,
+  // but `canBeSubtypeOf` doesn't nest for function types.
+  test_functionTypesAs_function_type_with_js_types_as() async {
+    await _testCasts([
+      _AsCast('JSAny Function()', 'JSAny Function()',
+          lint: false, unnecessary: true),
+      _AsCast('JSAny Function()', 'JSBoolean Function()', lint: false),
+      _AsCast('JSBoolean Function()', 'JSAny Function()', lint: false),
+      _AsCast('JSString Function()', 'JSBoolean Function()', lint: false),
+      _AsCast('void Function(JSAny)', 'void Function(JSAny)',
+          lint: false, unnecessary: true),
+      _AsCast('void Function(JSAny)', 'void Function(JSBoolean)', lint: false),
+      _AsCast('void Function(JSBoolean)', 'void Function(JSAny)', lint: false),
+      _AsCast('void Function(JSString)', 'void Function(JSBoolean)',
+          lint: false)
+    ]);
+  }
+
+  // TODO(srujzs): Some of the following type tests should result in an error,
+  // but `canBeSubtypeOf` doesn't nest for function types.
+  test_functionTypesIs_function_type_with_js_types_is() async {
+    await _testChecks([
+      _IsCheck('JSAny Function()', 'JSAny Function()',
+          lint: false, unnecessary: true),
+      _IsCheck('JSAny Function()', 'JSBoolean Function()', lint: false),
+      _IsCheck('JSBoolean Function()', 'JSAny Function()',
+          lint: false, unnecessary: true),
+      _IsCheck('JSString Function()', 'JSBoolean Function()', lint: false),
+      _IsCheck('void Function(JSAny)', 'void Function(JSAny)',
+          lint: false, unnecessary: true),
+      _IsCheck('void Function(JSAny)', 'void Function(JSBoolean)',
+          lint: false, unnecessary: true),
+      _IsCheck('void Function(JSBoolean)', 'void Function(JSAny)', lint: false),
+      _IsCheck('void Function(JSString)', 'void Function(JSBoolean)',
+          lint: false)
+    ]);
+  }
+
+  test_genericTypesAs_dart_type_generic_as_js_type_generic() async {
+    await _testCasts([_AsCast('List<int>', 'List<JSNumber>')]);
+  }
+
+  // For the purpose of this lint, generics on `dart:js_interop` types are
+  // ignored.
+  test_genericTypesAs_generic_js_type_as_generic_js_type() async {
+    await _testCasts([
+      _AsCast('JSArray<JSString>', 'JSArray<JSAny>', lint: false),
+      _AsCast('JSArray<JSAny>', 'JSArray<JSString>', lint: false),
+      _AsCast('JSArray<JSString>', 'JSArray<JSBoolean>', lint: false)
+    ]);
+  }
+
+  test_genericTypesAs_generic_type_with_multiple_type_parameter_as() async {
+    await _testCasts([
+      _AsCast('Map<JSBoolean, JSBoolean>', 'Map<JSBoolean, JSBoolean>',
+          lint: false, unnecessary: true),
+      _AsCast('Map<JSObject, JSString>', 'Map<JSAny, JSAny>', lint: false),
+      _AsCast('Map<JSObject, JSString>', 'Map<JSAny, JSBoolean>'),
+      _AsCast('Map<JSAny, JSString>', 'Map<JSObject, JSString>', lint: false),
+      _AsCast('Map<JSAny, JSString>', 'Map<JSObject, JSBoolean>')
+    ]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_dart_type_generic() async {
+    await _testCasts([_AsCast('List<JSString>', 'List<String>')]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_same_js_type_generic() async {
+    await _testCasts([
+      _AsCast('Set<JSString>', 'Set<JSString>', lint: false, unnecessary: true)
+    ]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_subtype_generic() async {
+    await _testCasts([_AsCast('List<JSAny>', 'List<JSObject>', lint: false)]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_supertype_generic() async {
+    await _testCasts(
+        [_AsCast('Future<JSArray>', 'Future<JSObject>', lint: false)]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_top_type_generic() async {
+    await _testCasts([
+      _AsCast('List<JSAny>', 'List<Object>', lint: false),
+      _AsCast('List<JSAny>', 'List<dynamic>', lint: false)
+    ]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_unrelated_js_type_generic() async {
+    await _testCasts([_AsCast('List<JSBoolean>', 'List<JSUint8List>')]);
+  }
+
+  test_genericTypesAs_js_type_generic_as_unrelated_js_type_generic_with_unrelated_container() async {
+    await _testCasts(
+        [_AsCast('List<JSBoolean>', 'Set<JSUint8List>', lint: false)]);
+  }
+
+  test_genericTypesAs_js_type_generic_with_container_inheritance_as() async {
+    await _testCasts([
+      _AsCast('List<JSBoolean>', 'Iterable<JSBoolean>', lint: false),
+      _AsCast('Iterable<JSObject>', 'List<JSObject>', lint: false),
+      _AsCast('List<JSBoolean>', 'Iterable<JSAny>', lint: false),
+      _AsCast('Iterable<JSArray>', 'List<JSObject>', lint: false),
+      _AsCast('List<JSAny>', 'Iterable<JSBoolean>', lint: false),
+      _AsCast('Iterable<JSObject>', 'List<JSArray>', lint: false),
+      // TODO(srujzs): These should be errors as there is a side cast within the
+      // generics, and you can't declare a class that subtypes both the left
+      // and right type, but `canBeSubtypeOf` doesn't nest in this case.
+      _AsCast('List<JSString>', 'Iterable<JSBoolean>', lint: false),
+      _AsCast('Iterable<JSBoolean>', 'List<JSString>', lint: false)
+    ]);
+  }
+
+  test_genericTypesAs_top_type_generic_as_js_type_generic() async {
+    await _testCasts([
+      _AsCast('List<Object>', 'List<JSAny>', lint: false),
+      _AsCast('List<dynamic>', 'List<JSArray>', lint: false)
+    ]);
+  }
+
+  test_genericTypesIs_dart_type_generic_is_js_type_generic() async {
+    await _testChecks([_IsCheck('List<int>', 'List<JSNumber>')]);
+  }
+
+  // For the purpose of this lint, generics on `dart:js_interop` types are
+  // ignored.
+  test_genericTypesIs_generic_js_type_is_generic_js_type() async {
+    await _testChecks([
+      _IsCheck('JSArray<JSString>', 'JSArray<JSAny>',
+          lint: false, unnecessary: true),
+      _IsCheck('JSArray<JSAny>', 'JSArray<JSString>', lint: false),
+      _IsCheck('JSArray<JSString>', 'JSArray<JSBoolean>', lint: false)
+    ]);
+  }
+
+  test_genericTypesIs_generic_type_with_multiple_type_parameter_is() async {
+    await _testChecks([
+      _IsCheck('Map<JSBoolean, JSBoolean>', 'Map<JSBoolean, JSBoolean>',
+          lint: false, unnecessary: true),
+      _IsCheck('Map<JSObject, JSString>', 'Map<JSAny, JSAny>',
+          lint: false, unnecessary: true),
+      _IsCheck('Map<JSObject, JSString>', 'Map<JSAny, JSBoolean>'),
+      _IsCheck('Map<JSAny, JSString>', 'Map<JSObject, JSString>'),
+      _IsCheck('Map<JSAny, JSString>', 'Map<JSObject, JSBoolean>')
+    ]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_dart_type_generic() async {
+    await _testChecks([_IsCheck('List<JSString>', 'List<String>')]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_same_js_type_generic() async {
+    await _testChecks([
+      _IsCheck('Set<JSString>', 'Set<JSString>', lint: false, unnecessary: true)
+    ]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_subtype_generic() async {
+    await _testChecks([_IsCheck('List<JSAny>', 'List<JSObject>')]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_supertype_generic() async {
+    await _testChecks([
+      _IsCheck('Future<JSArray>', 'Future<JSObject>',
+          lint: false, unnecessary: true)
+    ]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_top_type_generic() async {
+    await _testChecks([
+      _IsCheck('List<JSAny>', 'List<Object>', lint: false, unnecessary: true),
+      _IsCheck('List<JSAny>', 'List<dynamic>', lint: false, unnecessary: true)
+    ]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_unrelated_js_type_generic() async {
+    await _testChecks([_IsCheck('List<JSBoolean>', 'List<JSUint8List>')]);
+  }
+
+  test_genericTypesIs_js_type_generic_is_unrelated_js_type_generic_with_unrelated_container() async {
+    await _testChecks(
+        [_IsCheck('List<JSBoolean>', 'Set<JSUint8List>', lint: false)]);
+  }
+
+  test_genericTypesIs_js_type_generic_with_container_inheritance_is() async {
+    await _testChecks([
+      _IsCheck('List<JSBoolean>', 'Iterable<JSBoolean>',
+          lint: false, unnecessary: true),
+      _IsCheck('Iterable<JSObject>', 'List<JSObject>', lint: false),
+      _IsCheck('List<JSBoolean>', 'Iterable<JSAny>',
+          lint: false, unnecessary: true),
+      _IsCheck('Iterable<JSArray>', 'List<JSObject>', lint: false),
+      // TODO(srujzs): These should be errors as there is a subtype or unrelated
+      // check within the generics, and you can't declare a class that subtypes
+      // both the left and right type, but `canBeSubtypeOf` doesn't nest in this
+      // case.
+      _IsCheck('List<JSAny>', 'Iterable<JSBoolean>', lint: false),
+      _IsCheck('Iterable<JSObject>', 'List<JSArray>', lint: false),
+      _IsCheck('List<JSString>', 'Iterable<JSBoolean>', lint: false),
+      _IsCheck('Iterable<JSBoolean>', 'List<JSString>', lint: false)
+    ]);
+  }
+
+  test_genericTypesIs_top_type_generic_is_js_type_generic() async {
+    await _testChecks([
+      _IsCheck('List<Object>', 'List<JSAny>'),
+      _IsCheck('List<dynamic>', 'List<JSArray>')
+    ]);
+  }
+
+  test_nullabilityAs_js_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('JSAny?', 'JSAny', lint: false),
+      _AsCast('JSAny', 'JSAny?', lint: false),
+      _AsCast('JSAny?', 'JSAny?', lint: false, unnecessary: true),
+      _AsCast('JSAny?', 'JSArray', lint: false),
+      _AsCast('JSAny', 'JSArray?', lint: false),
+      _AsCast('JSAny?', 'JSArray?', lint: false),
+      _AsCast('JSArray', 'JSAny?', lint: false),
+      _AsCast('JSArray?', 'JSAny', lint: false),
+      _AsCast('JSArray?', 'JSAny?', lint: false),
+      _AsCast('JSString?', 'JSArray'),
+      _AsCast('JSString', 'JSArray?'),
+      _AsCast('JSString?', 'JSArray?')
+    ]);
+  }
+
+  test_nullabilityAs_null_as_js_type() async {
+    await assertDiagnostics('''
+    import 'dart:js_interop';
+
+    void foo() {
+      null as JSAny;
+      null as JSArray?;
+    }
+    ''', [error(WarningCode.CAST_FROM_NULL_ALWAYS_FAILS, 54, 13)]);
+  }
+
+  test_nullabilityIs_js_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('JSAny?', 'JSAny', lint: false),
+      _IsCheck('JSAny', 'JSAny?', lint: false, unnecessary: true),
+      _IsCheck('JSAny?', 'JSAny?', lint: false, unnecessary: true),
+      _IsCheck('JSAny?', 'JSArray'),
+      _IsCheck('JSAny', 'JSArray?'),
+      _IsCheck('JSAny?', 'JSArray?'),
+      _IsCheck('JSArray', 'JSAny?', lint: false, unnecessary: true),
+      _IsCheck('JSArray?', 'JSAny', lint: false),
+      _IsCheck('JSArray?', 'JSAny?', lint: false, unnecessary: true),
+      _IsCheck('JSString?', 'JSArray'),
+      _IsCheck('JSString', 'JSArray?'),
+      _IsCheck('JSString?', 'JSArray?')
+    ]);
+  }
+
+  test_nullabilityIs_null_is_js_type() async {
+    await assertDiagnostics('''
+    import 'dart:js_interop';
+
+    void foo() {
+      null is JSAny;
+      null is JSArray?;
+    }
+    ''', [error(WarningCode.UNNECESSARY_TYPE_CHECK_TRUE, 75, 16)]);
+  }
+
+  test_staticInteropAs_js_type_as_static_interop_type() async {
+    await _testCasts([
+      _AsCast('JSAny', 'A', lint: false),
+      _AsCast('JSObject', 'A', lint: false),
+      _AsCast('JSArray', 'A', lint: false),
+      _AsCast('JSBoolean', 'A')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropAs_static_interop_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('A', 'JSAny', lint: false),
+      _AsCast('A', 'JSObject', lint: false),
+      _AsCast('A', 'JSArray', lint: false),
+      _AsCast('A', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropAs_static_interop_type_as_unrelated_type() async {
+    await _testCasts([
+      _AsCast('A', 'String')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropAs_unrelated_type_as_static_interop_type() async {
+    await _testCasts([
+      _AsCast('String', 'A')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropIs_js_type_is_static_interop_type() async {
+    await _testChecks([
+      _IsCheck('JSAny', 'A'),
+      _IsCheck('JSObject', 'A', lint: false),
+      _IsCheck('JSArray', 'A', lint: false),
+      _IsCheck('JSBoolean', 'A')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropIs_static_interop_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('A', 'JSAny', lint: false),
+      _IsCheck('A', 'JSObject', lint: false),
+      _IsCheck('A', 'JSArray'),
+      _IsCheck('A', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropIs_static_interop_type_is_unrelated_type() async {
+    await _testChecks([
+      _IsCheck('A', 'String')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_staticInteropIs_unrelated_type_is_static_interop_type() async {
+    await _testChecks([
+      _IsCheck('String', 'A')
+    ], typeDeclarations: [
+      r'''
+      @JS()
+      @staticInterop
+      class A {}
+      '''
+    ]);
+  }
+
+  test_typeParametersAs_js_type_as_js_type_parameter() async {
+    await _testCasts([
+      _AsCast('JSAny', 'T', lint: false),
+      _AsCast('JSObject', 'T', lint: false),
+      _AsCast('JSBoolean', 'T'),
+      // This may or may not be a side cast, so warn.
+      _AsCast('JSArray', 'T')
+    ], typeParameters: [
+      'T extends JSObject'
+    ]);
+  }
+
+  test_typeParametersAs_js_type_parameter_as_js_type() async {
+    await _testCasts([
+      _AsCast('T', 'JSAny', lint: false),
+      _AsCast('T', 'JSObject', lint: false),
+      _AsCast('T', 'JSBoolean'),
+      // This may or may not be a side cast, so warn.
+      _AsCast('T', 'JSUint8List')
+    ], typeParameters: [
+      'T extends JSObject'
+    ]);
+  }
+
+  test_typeParametersAs_js_type_parameter_as_js_type_parameter() async {
+    await _testCasts([
+      _AsCast('T', 'T', lint: false, unnecessary: true),
+      _AsCast('U', 'U', lint: false, unnecessary: true),
+      _AsCast('V', 'V', lint: false, unnecessary: true),
+      _AsCast('T', 'U'),
+      _AsCast('U', 'T'),
+      _AsCast('T', 'V'),
+      _AsCast('V', 'T'),
+      _AsCast('U', 'V'),
+      _AsCast('V', 'U')
+    ], typeParameters: [
+      'T extends JSAny',
+      'U extends JSArray',
+      'V extends JSBoolean'
+    ]);
+  }
+
+  test_typeParametersAs_unrelated_type_as_js_type_parameter() async {
+    await _testCasts([_AsCast('String', 'T')],
+        typeParameters: ['T extends JSAny']);
+  }
+
+  test_typeParametersAs_unrelated_type_parameter_as_js_type() async {
+    await _testCasts([_AsCast('T', 'JSAny'), _AsCast('U', 'JSNumber')],
+        typeParameters: ['T', 'U extends int']);
+  }
+
+  test_typeParametersIs_js_type_is_js_type_parameter() async {
+    await _testChecks([
+      _IsCheck('JSAny', 'T'),
+      _IsCheck('JSObject', 'T'),
+      _IsCheck('JSBoolean', 'T'),
+      // This may or may not be an `is` check between unrelated types, so warn.
+      _IsCheck('JSArray', 'T')
+    ], typeParameters: [
+      'T extends JSObject'
+    ]);
+  }
+
+  test_typeParametersIs_js_type_parameter_is_js_type() async {
+    await _testChecks([
+      _IsCheck('T', 'JSAny', lint: false, unnecessary: true),
+      _IsCheck('T', 'JSObject', lint: false, unnecessary: true),
+      _IsCheck('T', 'JSBoolean'),
+      // This may or may not be an `is` check between unrelated types, so warn.
+      _IsCheck('T', 'JSUint8List')
+    ], typeParameters: [
+      'T extends JSObject'
+    ]);
+  }
+
+  test_typeParametersIs_js_type_parameter_is_js_type_parameter() async {
+    await _testChecks([
+      _IsCheck('T', 'T', lint: false, unnecessary: true),
+      _IsCheck('U', 'U', lint: false, unnecessary: true),
+      _IsCheck('V', 'V', lint: false, unnecessary: true),
+      _IsCheck('T', 'U'),
+      _IsCheck('U', 'T'),
+      _IsCheck('T', 'V'),
+      _IsCheck('V', 'T'),
+      _IsCheck('U', 'V'),
+      _IsCheck('V', 'U')
+    ], typeParameters: [
+      'T extends JSAny',
+      'U extends JSArray',
+      'V extends JSBoolean'
+    ]);
+  }
+
+  test_typeParametersIs_unrelated_type_is_js_type_parameter() async {
+    await _testChecks([_IsCheck('String', 'T')],
+        typeParameters: ['T extends JSAny']);
+  }
+
+  test_typeParametersIs_unrelated_type_parameter_is_js_type() async {
+    await _testChecks([_IsCheck('T', 'JSAny'), _IsCheck('U', 'JSNumber')],
+        typeParameters: ['T', 'U extends int']);
+  }
+
+  test_userInteropAs_nested_user_interop_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('B', 'JSAny', lint: false),
+      _AsCast('B', 'JSObject', lint: false),
+      _AsCast('B', 'JSArray', lint: false),
+      _AsCast('B', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSObject _) implements JSObject {}
+      ''',
+      r'''
+      extension type B(A _) implements A {}
+      '''
+    ]);
+  }
+
+  test_userInteropAs_user_interop_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('A', 'JSAny', lint: false),
+      _AsCast('A', 'JSObject', lint: false),
+      _AsCast('A', 'JSArray', lint: false),
+      _AsCast('A', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSObject _) {}
+      '''
+    ]);
+  }
+
+  test_userInteropAs_user_interop_type_parameter_as() async {
+    await _testCasts([
+      _AsCast('T', 'JSAny', lint: false),
+      _AsCast('U', 'JSObject', lint: false),
+      _AsCast('T', 'JSUint8List'),
+      _AsCast('U', 'JSArray')
+    ], typeParameters: [
+      'T extends A',
+      'U extends B'
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSTypedArray _) {}
+      ''',
+      r'''
+      @JS()
+      @staticInterop
+      class B {}
+      '''
+    ]);
+  }
+
+  test_userInteropIs_nested_user_interop_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('B', 'JSAny', lint: false, unnecessary: true),
+      _IsCheck('B', 'JSObject', lint: false, unnecessary: true),
+      _IsCheck('B', 'JSArray'),
+      _IsCheck('B', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSObject _) implements JSObject {}
+      ''',
+      r'''
+      extension type B(A _) implements A {}
+      '''
+    ]);
+  }
+
+  test_userInteropIs_user_interop_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('A', 'JSAny', lint: false),
+      _IsCheck('A', 'JSObject', lint: false),
+      _IsCheck('A', 'JSArray'),
+      _IsCheck('A', 'JSBoolean')
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSObject _) {}
+      '''
+    ]);
+  }
+
+  test_userInteropIs_user_interop_type_parameter_is() async {
+    await _testChecks([
+      _IsCheck('T', 'JSAny', lint: false),
+      _IsCheck('T', 'JSUint8List'),
+      _IsCheck('U', 'JSObject', lint: false),
+      _IsCheck('U', 'JSArray')
+    ], typeParameters: [
+      'T extends A',
+      'U extends B'
+    ], typeDeclarations: [
+      r'''
+      extension type A(JSTypedArray _) {}
+      ''',
+      r'''
+      @JS()
+      @staticInterop
+      class B {}
+      '''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesAs_dart_html_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('Event', 'JSAny', lint: false),
+      _AsCast('Event', 'JSString', lint: false),
+      _AsCast('Event', 'JSObject', lint: false)
+    ]);
+  }
+
+  test_wasmIncompatibleTypesAs_dart_js_type_as_js_type() async {
+    await _testCasts([_AsCast('JsObject', 'JSAny', lint: false)]);
+  }
+
+  test_wasmIncompatibleTypesAs_js_type_as_dart_html_type() async {
+    await _testCasts([
+      _AsCast('JSString', 'Event', lint: false),
+      _AsCast('JSString', 'Window', lint: false)
+    ]);
+  }
+
+  test_wasmIncompatibleTypesAs_js_type_as_dart_js_type() async {
+    await _testCasts([_AsCast('JSAny', 'JsObject', lint: false)]);
+  }
+
+  test_wasmIncompatibleTypesAs_js_type_as_package_js_type() async {
+    await _testCasts([
+      _AsCast('JSAny', 'A', lint: false),
+      _AsCast('JSString', 'B', lint: false)
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      ''',
+      r'''
+      @js.JS()
+      @js.staticInterop
+      class B {}'''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesAs_js_type_parameter_as() async {
+    await _testCasts([
+      _AsCast('T', 'JSAny', lint: false),
+      _AsCast('U', 'JSObject', lint: false),
+      _AsCast('V', 'JSArray', lint: false),
+      _AsCast('JSString', 'T', lint: false),
+      _AsCast('JSString', 'U', lint: false),
+      _AsCast('JSString', 'V', lint: false)
+    ], typeParameters: [
+      'T extends A',
+      'U extends Event',
+      'V extends JsObject'
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      '''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesAs_package_js_type_as_js_type() async {
+    await _testCasts([
+      _AsCast('A', 'JSBoolean', lint: false),
+      _AsCast('B', 'JSString', lint: false)
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      ''',
+      r'''
+      @js.JS()
+      @js.staticInterop
+      class B {}'''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesIs_dart_html_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('Event', 'JSAny', lint: false),
+      _IsCheck('Event', 'JSString', lint: false),
+      _IsCheck('Event', 'JSObject', lint: false)
+    ]);
+  }
+
+  test_wasmIncompatibleTypesIs_dart_js_type_is_js_type() async {
+    await _testChecks([_IsCheck('JsObject', 'JSAny', lint: false)]);
+  }
+
+  test_wasmIncompatibleTypesIs_js_type_is_dart_html_type() async {
+    await _testChecks([
+      _IsCheck('JSString', 'Event', lint: false),
+      _IsCheck('JSString', 'Window', lint: false)
+    ]);
+  }
+
+  test_wasmIncompatibleTypesIs_js_type_is_dart_js_type() async {
+    await _testChecks([_IsCheck('JSAny', 'JsObject', lint: false)]);
+  }
+
+  test_wasmIncompatibleTypesIs_js_type_is_package_js_type() async {
+    await _testChecks([
+      _IsCheck('JSAny', 'A', lint: false),
+      _IsCheck('JSString', 'B', lint: false)
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      ''',
+      r'''
+      @js.JS()
+      @js.staticInterop
+      class B {}'''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesIs_js_type_parameter_is() async {
+    await _testChecks([
+      _IsCheck('T', 'JSAny', lint: false),
+      _IsCheck('U', 'JSObject', lint: false),
+      _IsCheck('V', 'JSArray', lint: false),
+      _IsCheck('JSString', 'T', lint: false),
+      _IsCheck('JSString', 'U', lint: false),
+      _IsCheck('JSString', 'V', lint: false)
+    ], typeParameters: [
+      'T extends A',
+      'U extends Event',
+      'V extends JsObject'
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      '''
+    ]);
+  }
+
+  test_wasmIncompatibleTypesIs_package_js_type_is_js_type() async {
+    await _testChecks([
+      _IsCheck('A', 'JSBoolean', lint: false),
+      _IsCheck('B', 'JSString', lint: false)
+    ], typeDeclarations: [
+      r'''
+      @js.JS()
+      class A {}
+      ''',
+      r'''
+      @js.JS()
+      @js.staticInterop
+      class B {}'''
+    ]);
+  }
+
+  /// Given a list of JS interop [typeTests], constructs code to execute a type
+  /// test for each entry and then asserts that the lint and warning
+  /// expectations were correct for each type test.
+  ///
+  /// If [typeParameters] is not empty, declares the type parameters in the
+  /// function executing the tests.
+  ///
+  /// If [typeDeclarations] is not empty, declares the types before the function
+  /// executing the tests.
+  ///
+  /// If [cast] is true, uses `as` for the type test. Otherwise, uses `is`.
+  Future<void> _assertDiagnosticsWithTypeTests(List<_TypeTest> typeTests,
+      {required List<String> typeParameters,
+      required List<String> typeDeclarations,
+      required bool cast}) {
+    var lints = <ExpectedDiagnostic>[];
+    var code =
+        StringBuffer("// ignore: unused_import\nimport 'dart:html' hide JS;\n"
+            "// ignore: unused_import\nimport 'dart:js';\n"
+            "import 'dart:js_interop';\n"
+            "// ignore: unused_import\nimport 'package:js/js.dart' as js;\n");
+    for (var typeDeclaration in typeDeclarations) {
+      code.write('$typeDeclaration\n');
+    }
+    code.write('void foo');
+    if (typeParameters.isNotEmpty) code.write('<${typeParameters.join(',')}>');
+    code.write('(');
+    var tempVarCount = 1;
+    for (var typeTest in typeTests) {
+      code.write('${typeTest.valueType} t${tempVarCount++}, ');
+    }
+    code.write(') {\n');
+    tempVarCount = 1;
+    for (var typeTest in typeTests) {
+      var test = 't${tempVarCount++} ${cast ? 'as' : 'is'} ${typeTest.type}';
+      if (typeTest.lint) lints.add(lint(code.length, test.length));
+      if (typeTest.unnecessary) {
+        if (cast) {
+          lints.add(
+              error(WarningCode.UNNECESSARY_CAST, code.length, test.length));
+        } else {
+          lints.add(error(WarningCode.UNNECESSARY_TYPE_CHECK_TRUE, code.length,
+              test.length));
+        }
+      }
+      code.write('$test;\n');
+    }
+    code.write('}\n');
+    return assertDiagnostics(code.toString(), lints);
+  }
+
+  Future<void> _testCasts(List<_AsCast> typeTests,
+          {List<String> typeParameters = const [],
+          List<String> typeDeclarations = const []}) =>
+      _assertDiagnosticsWithTypeTests(typeTests,
+          typeParameters: typeParameters,
+          typeDeclarations: typeDeclarations,
+          cast: true);
+
+  Future<void> _testChecks(List<_IsCheck> typeTests,
+          {List<String> typeParameters = const [],
+          List<String> typeDeclarations = const []}) =>
+      _assertDiagnosticsWithTypeTests(typeTests,
+          typeDeclarations: typeDeclarations,
+          typeParameters: typeParameters,
+          cast: false);
+}
+
+/// Represents an `as` cast from a value of type [valueType] to [type].
+final class _AsCast extends _TypeTest {
+  _AsCast(super.valueType, super.type, {super.lint, super.unnecessary});
+}
+
+/// Represents an `is` check against [type] where the value is of type
+/// [valueType].
+final class _IsCheck extends _TypeTest {
+  _IsCheck(super.valueType, super.type, {super.lint, super.unnecessary});
+}
+
+/// Represents a type test using a runtime check.
+abstract class _TypeTest {
+  String valueType;
+  String type;
+
+  /// Whether this type test should emit a lint.
+  bool lint;
+
+  /// Whether this type test was trivially true, which determines whether the
+  /// test should ignore the related warning.
+  bool unnecessary;
+
+  _TypeTest(this.valueType, this.type,
+      {this.lint = true, this.unnecessary = false});
+}
diff --git a/pkg/linter/tool/since/sdk.yaml b/pkg/linter/tool/since/sdk.yaml
index 6984a3f..bc0d5fb 100644
--- a/pkg/linter/tool/since/sdk.yaml
+++ b/pkg/linter/tool/since/sdk.yaml
@@ -83,6 +83,7 @@
 implicit_call_tearoffs: 2.19.0
 implicit_reopen: 3.0.0
 invalid_case_patterns: 3.0.0
+invalid_runtime_check_with_js_interop_types: 3.4.0-wip
 invariant_booleans: 2.0.0
 iterable_contains_unrelated_type: 2.0.0
 join_return_with_assignment: 2.0.0