Skip to content

Commit 9fae030

Browse files
srujzsCommit Queue
authored andcommitted
[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 <[email protected]> Commit-Queue: Srujan Gaddam <[email protected]>
1 parent 8022910 commit 9fae030

File tree

1 file changed

+81
-36
lines changed

1 file changed

+81
-36
lines changed

pkg/linter/lib/src/rules/invalid_runtime_check_with_js_interop_types.dart

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import 'package:analyzer/src/dart/element/type.dart';
1212
// ignore: implementation_imports
1313
import '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

1618
import '../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+
173196
class 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+
203240
class _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

Comments
 (0)