[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