[linter] Early exit if types do not contain JS interop types in invalid_runtime_check_with_js_interop_types

Bug: https://github.com/dart-lang/linter/issues/5106

Adds a type visitor that checks if a type contains a JS interop
type, and avoids an expensive canBeSubtypeOf check if neither
the left nor right type contains a JS interop type. Unifies
some logic in checking whether a type is a JS interop type as
well.

Change-Id: I8e5a6d6da411d533fc801890d520a6e02f28ebb5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/389761
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Srujan Gaddam <srujzs@google.com>
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
index d01f661..6d5d496 100644
--- 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
@@ -12,6 +12,8 @@
 // ignore: implementation_imports
 import 'package:analyzer/src/dart/element/type_system.dart'
     show TypeSystemImpl, ExtensionTypeErasure;
+// ignore: implementation_imports
+import 'package:analyzer/src/dart/element/type_visitor.dart';
 
 import '../analyzer.dart';
 
@@ -31,33 +33,27 @@
   'dart:web_gl',
 };
 
-/// 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);
+/// Given a [type], determine if it is a JS interop type that corresponds to
+/// [kind].
+bool _isJsInteropType(DartType type, _InteropTypeKind kind) {
+  if (type is TypeParameterType) return _isJsInteropType(type.bound, kind);
   if (type is InterfaceType) {
     var element = type.element;
+    var dartJsInteropTypeKind = kind == _InteropTypeKind.dartJsInteropType ||
+        kind == _InteropTypeKind.any;
+    var userJsInteropTypeKind = kind == _InteropTypeKind.userJsInteropType ||
+        kind == _InteropTypeKind.any;
     if (element is ExtensionTypeElement) {
-      return element.isFromLibrary(_dartJsInteropUri);
-    }
-  }
-  return false;
-}
-
-/// Whether [type] is a user JS interop type using `dart:js_interop` or is bound
-/// to one.
-///
-/// An interop type is a user interop type if it's an extension type on another
-/// interop type or a `dart:js_interop` `@staticInterop` type.
-bool _isUserJsInteropType(DartType type) {
-  if (type is TypeParameterType) return _isUserJsInteropType(type.bound);
-  if (type is InterfaceType) {
-    var element = type.element;
-    if (element is ExtensionTypeElement) {
-      var representationType = element.representation.type;
-      return _isDartJsInteropType(representationType) ||
-          _isUserJsInteropType(representationType);
-    } else if (_jsTypeForStaticInterop(type) != null) {
+      if (dartJsInteropTypeKind && element.isFromLibrary(_dartJsInteropUri)) {
+        return true;
+      } else if (userJsInteropTypeKind) {
+        var representationType = element.representation.type;
+        return _isJsInteropType(
+                representationType, _InteropTypeKind.dartJsInteropType) ||
+            _isJsInteropType(
+                representationType, _InteropTypeKind.userJsInteropType);
+      }
+    } else if (userJsInteropTypeKind && _jsTypeForStaticInterop(type) != null) {
       return true;
     }
   }
@@ -144,8 +140,8 @@
   @override
   DartType? visitInterfaceType(covariant InterfaceTypeImpl type) {
     if (_keepUserInteropTypes
-        ? _isUserJsInteropType(type)
-        : _isDartJsInteropType(type)) {
+        ? _isJsInteropType(type, _InteropTypeKind.userJsInteropType)
+        : _isJsInteropType(type, _InteropTypeKind.dartJsInteropType)) {
       // Nullability and generics on interop types are ignored for this lint. In
       // order to just compare the interfaces themselves, we use `thisType`.
       return type.element.thisType;
@@ -170,6 +166,33 @@
   }
 }
 
+/// Determines whether a given [DartType] is or contains a type that is a JS
+/// interop type.
+class InteropTypeChecker extends RecursiveTypeVisitor {
+  bool _hasInteropType = false;
+  final _visitedTypes = <DartType>{};
+
+  bool hasInteropType(DartType type) {
+    _hasInteropType = false;
+    _visitedTypes.clear();
+    type.accept(this);
+    return _hasInteropType;
+  }
+
+  @override
+  bool visitInterfaceType(InterfaceType type) {
+    _hasInteropType |= _isJsInteropType(type, _InteropTypeKind.any);
+    return super.visitInterfaceType(type);
+  }
+
+  @override
+  bool visitTypeParameterType(TypeParameterType type) {
+    // Visiting the bound may result in a cycle e.g. `class C<T extends C<T>>`.
+    if (_visitedTypes.add(type)) return type.bound.accept(this);
+    return super.visitTypeParameterType(type);
+  }
+}
+
 class InvalidRuntimeCheckWithJSInteropTypes extends LintRule {
   InvalidRuntimeCheckWithJSInteropTypes()
       : super(
@@ -200,14 +223,29 @@
   }
 }
 
+/// The kind of JS interop type to check for.
+///
+/// [dartJsInteropType] corresponds to either an extension type from
+/// `dart:js_interop` or a type parameter that is bound to one.
+/// [userJsInteropType] corresponds to either an extension type whose
+/// representation type is a JS interop type, an `@staticInterop` type, or a
+/// type parameter that is bound to either.
+/// [any] corresponds to either a [dartJsInteropType] or [userJsInteropType].
+enum _InteropTypeKind {
+  dartJsInteropType,
+  userJsInteropType,
+  any,
+}
+
 class _Visitor extends SimpleAstVisitor<void> {
   final LintRule rule;
   final TypeSystemImpl typeSystem;
-  final EraseNonJSInteropTypes _eraseNonJsInteropTypes;
+  final EraseNonJSInteropTypes eraseNonJsInteropTypes =
+      EraseNonJSInteropTypes();
+  final InteropTypeChecker interopTypeChecker = InteropTypeChecker();
 
   _Visitor(this.rule, TypeSystem typeSystem)
-      : typeSystem = typeSystem as TypeSystemImpl,
-        _eraseNonJsInteropTypes = EraseNonJSInteropTypes();
+      : typeSystem = typeSystem as TypeSystemImpl;
 
   /// 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
@@ -252,11 +290,13 @@
     LintCode? lintCode;
     (DartType, DartType) eraseTypes(DartType left, DartType right) {
       var erasedLeft =
-          typeSystem.promoteToNonNull(_eraseNonJsInteropTypes.perform(left));
+          typeSystem.promoteToNonNull(eraseNonJsInteropTypes.perform(left));
       var erasedRight =
-          typeSystem.promoteToNonNull(_eraseNonJsInteropTypes.perform(right));
-      var leftIsInteropType = _isDartJsInteropType(erasedLeft);
-      var rightIsInteropType = _isDartJsInteropType(erasedRight);
+          typeSystem.promoteToNonNull(eraseNonJsInteropTypes.perform(right));
+      var leftIsInteropType =
+          _isJsInteropType(erasedLeft, _InteropTypeKind.dartJsInteropType);
+      var rightIsInteropType =
+          _isJsInteropType(erasedRight, _InteropTypeKind.dartJsInteropType);
       // 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.
@@ -288,11 +328,11 @@
               // Only report if the right type is a user JS interop type.
               // Checks like `window is JSAny` are not confusing and not worth
               // linting.
-              if (_isUserJsInteropType(right) &&
+              if (_isJsInteropType(right, _InteropTypeKind.userJsInteropType) &&
                   !typeSystem.isSubtypeOf(
-                      _eraseNonJsInteropTypes.perform(left,
+                      eraseNonJsInteropTypes.perform(left,
                           keepUserInteropTypes: true),
-                      _eraseNonJsInteropTypes.perform(right,
+                      eraseNonJsInteropTypes.perform(right,
                           keepUserInteropTypes: true))) {
                 lintCode = LinterLintCode
                     .invalid_runtime_check_with_js_interop_types_js_is_unrelated_js;
@@ -325,6 +365,11 @@
       return (erasedLeft, erasedRight);
     }
 
+    // If there are no relevant interop types, exit early.
+    if (!interopTypeChecker.hasInteropType(leftType) &&
+        !interopTypeChecker.hasInteropType(rightType)) {
+      return lintCode;
+    }
     // Called here for the side effects of `eraseTypes`.
     typeSystem.canBeSubtypeOf(leftType, rightType, eraseTypes: eraseTypes);
     return lintCode;