blob: 445be18a75db1ccad24fe05d8deba53f68041803 [file] [log] [blame]
// Copyright (c) 2021, 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.
// @dart=2.9
// Tests inheritance relationships between `JS` and `anonymous` classes/objects.
@JS()
library extends_test;
import 'package:expect/minitest.dart';
import 'package:js/js.dart';
@JS()
external void eval(String code);
@JS()
class JSClass {
external JSClass();
external int get a;
external int getA();
external int getAOrB();
}
@JS()
@anonymous
class AnonymousClass {
external int get a;
external int getA();
}
@JS()
class JSExtendJSClass extends JSClass {
external JSExtendJSClass(int a, int b);
external int get b;
external int getB();
external int getAOrB();
}
@JS()
class JSExtendAnonymousClass extends AnonymousClass {
external JSExtendAnonymousClass(int a, int b);
external int get b;
external int getB();
}
@JS()
@anonymous
class AnonymousExtendAnonymousClass extends AnonymousClass {
external int get b;
external int getB();
}
@JS()
@anonymous
class AnonymousExtendJSClass extends JSClass {
external int get b;
external int getB();
external int getAOrB();
}
external AnonymousClass get anon;
external AnonymousExtendAnonymousClass get anonExtendAnon;
external AnonymousExtendJSClass get anonExtendJS;
void setUpWithoutES6Syntax() {
// Use the old way to define inheritance between JS objects.
eval(r"""
function inherits(child, parent) {
if (child.prototype.__proto__) {
child.prototype.__proto__ = parent.prototype;
} else {
function tmp() {};
tmp.prototype = parent.prototype;
child.prototype = new tmp();
child.prototype.constructor = child;
}
}
function JSClass(a) {
if (arguments.length == 0) a = 1;
this.a = a;
this.getA = function() {
return this.a;
}
this.getAOrB = function() {
return this.getA();
}
}
function JSExtendJSClass(a, b) {
JSClass.call(this, a);
this.b = b;
this.getB = function() {
return this.b;
}
this.getAOrB = function() {
return this.getB();
}
}
inherits(JSExtendJSClass, JSClass);
function JSExtendAnonymousClass(a, b) {
this.a = a;
this.b = b;
this.getA = function() {
return this.a;
}
this.getB = function() {
return this.b;
}
this.getAOrB = function() {
return this.getB();
}
}
self.anon = new JSClass(1);
self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
self.anonExtendJS = new JSExtendJSClass(1, 2);
""");
}
void setUpWithES6Syntax() {
// Use the ES6 syntax for classes to make inheritance easier.
eval(r"""
class JSClass {
constructor(a) {
if (arguments.length == 0) a = 1;
this.a = a;
}
getA() {
return this.a;
}
getAOrB() {
return this.getA();
}
}
self.JSClass = JSClass;
class JSExtendJSClass extends JSClass {
constructor(a, b) {
super(a);
this.b = b;
}
getB() {
return this.b;
}
getAOrB() {
return this.getB();
}
}
self.JSExtendJSClass = JSExtendJSClass;
class JSExtendAnonymousClass {
constructor(a, b) {
this.a = a;
this.b = b;
}
getA() {
return this.a;
}
getB() {
return this.b;
}
getAOrB() {
return this.getB();
}
}
self.JSExtendAnonymousClass = JSExtendAnonymousClass;
self.anon = new JSClass(1);
self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
self.anonExtendJS = new JSExtendJSClass(1, 2);
""");
}
void testInheritance() {
var jsExtendJS = JSExtendJSClass(1, 2);
expect(jsExtendJS.a, 1);
expect(jsExtendJS.b, 2);
expect(jsExtendJS.getA(), 1);
expect(jsExtendJS.getB(), 2);
// Test method overrides.
expect(jsExtendJS.getAOrB(), 2);
expect((jsExtendJS as JSClass).getAOrB(), 2);
var jsExtendAnon = JSExtendAnonymousClass(1, 2);
expect(jsExtendAnon.a, 1);
expect(jsExtendAnon.b, 2);
expect(jsExtendAnon.getA(), 1);
expect(jsExtendAnon.getB(), 2);
expect(anonExtendAnon.a, 1);
expect(anonExtendAnon.b, 2);
expect(anonExtendAnon.getA(), 1);
expect(anonExtendAnon.getB(), 2);
expect(anonExtendJS.a, 1);
expect(anonExtendJS.b, 2);
expect(anonExtendJS.getA(), 1);
expect(anonExtendJS.getB(), 2);
expect(anonExtendJS.getAOrB(), 2);
expect((anonExtendJS as JSClass).getAOrB(), 2);
// Test type checking and casts succeeds regardless of type hierarchy.
// Test type checking at runtime by disabling inlining. We still, however, do
// `is` checks directly below to test those optimizations.
@pragma('dart2js:noInline')
void runtimeIsAndAs<T>(instance) {
expect(instance is T, true);
expect(() => instance as T, returnsNormally);
}
// Test that base JS type can be used as any subtype.
var js = JSClass();
expect(js is JSExtendJSClass, true);
runtimeIsAndAs<JSExtendJSClass>(js);
expect(js is AnonymousExtendJSClass, true);
runtimeIsAndAs<AnonymousExtendJSClass>(js);
// Test that base anonymous type can be use as any subtype.
// Conversion from external getter value to a variable is needed to coerce
// compile time optimization of type checks. This applies for below as well.
var anonVar = anon;
expect(anonVar is JSExtendAnonymousClass, true);
runtimeIsAndAs<JSExtendAnonymousClass>(anonVar);
expect(anonVar is AnonymousExtendAnonymousClass, true);
runtimeIsAndAs<AnonymousExtendAnonymousClass>(anonVar);
// Test that instance of subtypes can be used as their JS supertype.
expect(jsExtendJS is JSClass, true);
runtimeIsAndAs<JSClass>(jsExtendJS);
var anonExtendJSVar = anonExtendJS;
expect(anonExtendJSVar is JSClass, true);
runtimeIsAndAs<JSClass>(anonExtendJSVar);
// Test that instance of subtypes can be used as their anonymous supertype.
var anonExtendAnonVar = anonExtendAnon;
expect(anonExtendAnonVar is AnonymousClass, true);
runtimeIsAndAs<AnonymousClass>(anonExtendAnonVar);
expect(jsExtendAnon is AnonymousClass, true);
runtimeIsAndAs<AnonymousClass>(jsExtendAnon);
}
JSClass returnJS() => throw '';
AnonymousClass returnAnon() => throw '';
JSExtendJSClass returnJSExtendJS() => throw '';
JSExtendAnonymousClass returnJSExtendAnon() => throw '';
AnonymousExtendJSClass returnAnonExtendJS() => throw '';
AnonymousExtendAnonymousClass returnAnonExtendAnon() => throw '';
@pragma('dart2js:noInline')
void isRuntimeSubtypeBothWays<T, U>() {
// Test T <: U and U <: T. With interop types, type checks should pass
// regardless of type hierarchy. Note that dart2js does these type checks at
// runtime. Below, we do compile-time checks using top-level functions.
T f1() => throw '';
U f2() => throw '';
expect(f1 is U Function(), true);
expect(f2 is T Function(), true);
}
void testSubtyping() {
// Test subtyping for inheritance between JS and anonymous classes.
expect(returnJS is JSExtendJSClass Function(), true);
expect(returnJSExtendJS is JSClass Function(), true);
isRuntimeSubtypeBothWays<JSClass, JSExtendJSClass>();
expect(returnJS is AnonymousExtendJSClass Function(), true);
expect(returnAnonExtendJS is JSClass Function(), true);
isRuntimeSubtypeBothWays<JSClass, AnonymousExtendJSClass>();
expect(returnAnon is JSExtendAnonymousClass Function(), true);
expect(returnJSExtendAnon is AnonymousClass Function(), true);
isRuntimeSubtypeBothWays<AnonymousClass, JSExtendAnonymousClass>();
expect(returnAnon is AnonymousExtendAnonymousClass Function(), true);
expect(returnAnonExtendAnon is AnonymousClass Function(), true);
isRuntimeSubtypeBothWays<AnonymousClass, AnonymousExtendAnonymousClass>();
}