@@ -12,6 +12,8 @@ import 'package:analyzer/src/dart/element/type.dart';
1212// ignore: implementation_imports
1313import 'package:analyzer/src/dart/element/type_system.dart'
1414 show TypeSystemImpl, ExtensionTypeErasure;
15+ // ignore: implementation_imports
16+ import 'package:analyzer/src/dart/element/type_visitor.dart' ;
1517
1618import '../analyzer.dart' ;
1719
@@ -31,33 +33,27 @@ const Set<String> _sdkWebLibraries = {
3133 'dart:web_gl' ,
3234};
3335
34- /// Given a [type] erased by [EraseNonJSInteropTypes] , determine if it is a type
35- /// that is a `dart:js_interop` type or is bound to one.
36- bool _isDartJsInteropType (DartType type) {
37- if (type is TypeParameterType ) return _isDartJsInteropType (type.bound);
38- if (type is InterfaceType ) {
39- var element = type.element;
40- if (element is ExtensionTypeElement ) {
41- return element.isFromLibrary (_dartJsInteropUri);
42- }
43- }
44- return false ;
45- }
46-
47- /// Whether [type] is a user JS interop type using `dart:js_interop` or is bound
48- /// to one.
49- ///
50- /// An interop type is a user interop type if it's an extension type on another
51- /// interop type or a `dart:js_interop` `@staticInterop` type.
52- bool _isUserJsInteropType (DartType type) {
53- if (type is TypeParameterType ) return _isUserJsInteropType (type.bound);
36+ /// Given a [type] , determine if it is a JS interop type that corresponds to
37+ /// [kind] .
38+ bool _isJsInteropType (DartType type, _InteropTypeKind kind) {
39+ if (type is TypeParameterType ) return _isJsInteropType (type.bound, kind);
5440 if (type is InterfaceType ) {
5541 var element = type.element;
42+ var dartJsInteropTypeKind = kind == _InteropTypeKind .dartJsInteropType ||
43+ kind == _InteropTypeKind .any;
44+ var userJsInteropTypeKind = kind == _InteropTypeKind .userJsInteropType ||
45+ kind == _InteropTypeKind .any;
5646 if (element is ExtensionTypeElement ) {
57- var representationType = element.representation.type;
58- return _isDartJsInteropType (representationType) ||
59- _isUserJsInteropType (representationType);
60- } else if (_jsTypeForStaticInterop (type) != null ) {
47+ if (dartJsInteropTypeKind && element.isFromLibrary (_dartJsInteropUri)) {
48+ return true ;
49+ } else if (userJsInteropTypeKind) {
50+ var representationType = element.representation.type;
51+ return _isJsInteropType (
52+ representationType, _InteropTypeKind .dartJsInteropType) ||
53+ _isJsInteropType (
54+ representationType, _InteropTypeKind .userJsInteropType);
55+ }
56+ } else if (userJsInteropTypeKind && _jsTypeForStaticInterop (type) != null ) {
6157 return true ;
6258 }
6359 }
@@ -144,8 +140,8 @@ class EraseNonJSInteropTypes extends ExtensionTypeErasure {
144140 @override
145141 DartType ? visitInterfaceType (covariant InterfaceTypeImpl type) {
146142 if (_keepUserInteropTypes
147- ? _isUserJsInteropType (type)
148- : _isDartJsInteropType (type)) {
143+ ? _isJsInteropType (type, _InteropTypeKind .userJsInteropType )
144+ : _isJsInteropType (type, _InteropTypeKind .dartJsInteropType )) {
149145 // Nullability and generics on interop types are ignored for this lint. In
150146 // order to just compare the interfaces themselves, we use `thisType`.
151147 return type.element.thisType;
@@ -170,6 +166,33 @@ class EraseNonJSInteropTypes extends ExtensionTypeErasure {
170166 }
171167}
172168
169+ /// Determines whether a given [DartType] is or contains a type that is a JS
170+ /// interop type.
171+ class InteropTypeChecker extends RecursiveTypeVisitor {
172+ bool _hasInteropType = false ;
173+ final _visitedTypes = < DartType > {};
174+
175+ bool hasInteropType (DartType type) {
176+ _hasInteropType = false ;
177+ _visitedTypes.clear ();
178+ type.accept (this );
179+ return _hasInteropType;
180+ }
181+
182+ @override
183+ bool visitInterfaceType (InterfaceType type) {
184+ _hasInteropType | = _isJsInteropType (type, _InteropTypeKind .any);
185+ return super .visitInterfaceType (type);
186+ }
187+
188+ @override
189+ bool visitTypeParameterType (TypeParameterType type) {
190+ // Visiting the bound may result in a cycle e.g. `class C<T extends C<T>>`.
191+ if (_visitedTypes.add (type)) return type.bound.accept (this );
192+ return super .visitTypeParameterType (type);
193+ }
194+ }
195+
173196class InvalidRuntimeCheckWithJSInteropTypes extends LintRule {
174197 InvalidRuntimeCheckWithJSInteropTypes ()
175198 : super (
@@ -200,14 +223,29 @@ class InvalidRuntimeCheckWithJSInteropTypes extends LintRule {
200223 }
201224}
202225
226+ /// The kind of JS interop type to check for.
227+ ///
228+ /// [dartJsInteropType] corresponds to either an extension type from
229+ /// `dart:js_interop` or a type parameter that is bound to one.
230+ /// [userJsInteropType] corresponds to either an extension type whose
231+ /// representation type is a JS interop type, an `@staticInterop` type, or a
232+ /// type parameter that is bound to either.
233+ /// [any] corresponds to either a [dartJsInteropType] or [userJsInteropType] .
234+ enum _InteropTypeKind {
235+ dartJsInteropType,
236+ userJsInteropType,
237+ any,
238+ }
239+
203240class _Visitor extends SimpleAstVisitor <void > {
204241 final LintRule rule;
205242 final TypeSystemImpl typeSystem;
206- final EraseNonJSInteropTypes _eraseNonJsInteropTypes;
243+ final EraseNonJSInteropTypes eraseNonJsInteropTypes =
244+ EraseNonJSInteropTypes ();
245+ final InteropTypeChecker interopTypeChecker = InteropTypeChecker ();
207246
208247 _Visitor (this .rule, TypeSystem typeSystem)
209- : typeSystem = typeSystem as TypeSystemImpl ,
210- _eraseNonJsInteropTypes = EraseNonJSInteropTypes ();
248+ : typeSystem = typeSystem as TypeSystemImpl ;
211249
212250 /// Determines if a type test from [leftType] to [rightType] is a valid test
213251 /// for JS interop, and if not, returns the [LintCode] associated with the
@@ -252,11 +290,13 @@ class _Visitor extends SimpleAstVisitor<void> {
252290 LintCode ? lintCode;
253291 (DartType , DartType ) eraseTypes (DartType left, DartType right) {
254292 var erasedLeft =
255- typeSystem.promoteToNonNull (_eraseNonJsInteropTypes .perform (left));
293+ typeSystem.promoteToNonNull (eraseNonJsInteropTypes .perform (left));
256294 var erasedRight =
257- typeSystem.promoteToNonNull (_eraseNonJsInteropTypes.perform (right));
258- var leftIsInteropType = _isDartJsInteropType (erasedLeft);
259- var rightIsInteropType = _isDartJsInteropType (erasedRight);
295+ typeSystem.promoteToNonNull (eraseNonJsInteropTypes.perform (right));
296+ var leftIsInteropType =
297+ _isJsInteropType (erasedLeft, _InteropTypeKind .dartJsInteropType);
298+ var rightIsInteropType =
299+ _isJsInteropType (erasedRight, _InteropTypeKind .dartJsInteropType);
260300 // If there's already an invalid check in this `canBeSubtypeOf` check, we
261301 // are already going to lint, so only continue checking if we haven't
262302 // found an issue.
@@ -288,11 +328,11 @@ class _Visitor extends SimpleAstVisitor<void> {
288328 // Only report if the right type is a user JS interop type.
289329 // Checks like `window is JSAny` are not confusing and not worth
290330 // linting.
291- if (_isUserJsInteropType (right) &&
331+ if (_isJsInteropType (right, _InteropTypeKind .userJsInteropType ) &&
292332 ! typeSystem.isSubtypeOf (
293- _eraseNonJsInteropTypes .perform (left,
333+ eraseNonJsInteropTypes .perform (left,
294334 keepUserInteropTypes: true ),
295- _eraseNonJsInteropTypes .perform (right,
335+ eraseNonJsInteropTypes .perform (right,
296336 keepUserInteropTypes: true ))) {
297337 lintCode = LinterLintCode
298338 .invalid_runtime_check_with_js_interop_types_js_is_unrelated_js;
@@ -325,6 +365,11 @@ class _Visitor extends SimpleAstVisitor<void> {
325365 return (erasedLeft, erasedRight);
326366 }
327367
368+ // If there are no relevant interop types, exit early.
369+ if (! interopTypeChecker.hasInteropType (leftType) &&
370+ ! interopTypeChecker.hasInteropType (rightType)) {
371+ return lintCode;
372+ }
328373 // Called here for the side effects of `eraseTypes`.
329374 typeSystem.canBeSubtypeOf (leftType, rightType, eraseTypes: eraseTypes);
330375 return lintCode;
0 commit comments