Support user generated custom native JS classes.

BUG=
R=jmesserly@google.com, sra@google.com

Review URL: https://codereview.chromium.org//1318043005 .
This commit is contained in:
Jacob Richman 2015-10-13 13:15:28 -07:00
parent d565c5361e
commit cd2752431e
38 changed files with 1652 additions and 329 deletions

View file

@ -45,7 +45,7 @@ import '../library_loader.dart' show
import '../native/native.dart' as native show
NativeEnqueuer;
import '../patch_parser.dart' show
checkNativeAnnotation;
checkNativeAnnotation, checkJsInteropAnnotation;
import '../resolution/tree_elements.dart' show
TreeElements;
import '../tree/tree.dart' show
@ -293,6 +293,16 @@ abstract class Backend {
}
});
}
checkJsInteropAnnotation(compiler, library);
library.forEachLocalMember((Element element) {
checkJsInteropAnnotation(compiler, element);
if (element.isClass && element.isJsInterop) {
ClassElement classElement = element;
classElement.forEachMember((_, memberElement) {
checkJsInteropAnnotation(compiler, memberElement);
});
}
});
return new Future.value();
}

View file

@ -1089,7 +1089,7 @@ class TransformingVisitor extends DeepRecursiveVisitor {
if (target is! FieldElement) return false;
// TODO(asgerf): Inlining native fields will make some tests pass for the
// wrong reason, so for testing reasons avoid inlining them.
if (target.isNative) return false;
if (target.isNative || target.isJsInterop) return false;
Continuation cont = node.continuation.definition;
if (node.selector.isGetter) {
GetField get = new GetField(getDartReceiver(node), target);

View file

@ -275,6 +275,8 @@ enum MessageKind {
INVALID_UNNAMED_CONSTRUCTOR_NAME,
INVALID_URI,
INVALID_USE_OF_SUPER,
JS_INTEROP_CLASS_CANNOT_EXTEND_DART_CLASS,
JS_INTEROP_CLASS_NON_EXTERNAL_MEMBER,
LIBRARY_NAME_MISMATCH,
LIBRARY_NOT_FOUND,
LIBRARY_NOT_SUPPORTED,
@ -2130,6 +2132,45 @@ main() => A.A = 1;
const MessageTemplate(MessageKind.INTERNAL_LIBRARY,
"Internal library '#{resolvedUri}' is not accessible."),
MessageKind.JS_INTEROP_CLASS_CANNOT_EXTEND_DART_CLASS:
const MessageTemplate(
MessageKind.JS_INTEROP_CLASS_CANNOT_EXTEND_DART_CLASS,
"Js-interop class '#{cls}' cannot extend from the non js-interop "
"class '#{superclass}'.",
howToFix: "Annotate the superclass with @Js.",
examples: const [
"""
import 'package:js/js.dart';
class Foo { }
@Js()
class Bar extends Foo { }
main() {
new Bar();
}
"""]),
MessageKind.JS_INTEROP_CLASS_NON_EXTERNAL_MEMBER:
const MessageTemplate(
MessageKind.JS_INTEROP_CLASS_NON_EXTERNAL_MEMBER,
"Member '#{member}' in js-interop class '#{cls}' is not external.",
howToFix: "Mark all interop methods external",
examples: const [
"""
import 'package:js/js.dart';
@Js()
class Foo {
bar() {}
}
main() {
new Foo().bar();
}
"""]),
MessageKind.LIBRARY_NOT_FOUND:
const MessageTemplate(MessageKind.LIBRARY_NOT_FOUND,
"Library not found '#{resolvedUri}'."),

View file

@ -306,6 +306,8 @@ abstract class Element implements Entity {
bool get isTopLevel;
bool get isAssignable;
bool get isNative;
bool get isJsInterop;
bool get isDeferredLoaderGetter;
/// True if the element is declared in a patch library but has no
@ -402,6 +404,8 @@ abstract class Element implements Entity {
bool get hasFixedBackendName;
String get fixedBackendName;
String get jsInteropName;
bool get isAbstract;
Scope buildScope();
@ -411,6 +415,9 @@ abstract class Element implements Entity {
AnalyzableElement get analyzableElement;
accept(ElementVisitor visitor, arg);
void setJsInteropName(String name);
void markAsJsInterop();
}
class Elements {
@ -515,7 +522,7 @@ class Elements {
static bool isNativeOrExtendsNative(ClassElement element) {
if (element == null) return false;
if (element.isNative) return true;
if (element.isNative || element.isJsInterop) return true;
assert(element.isResolved);
return isNativeOrExtendsNative(element.superclass);
}

View file

@ -234,9 +234,54 @@ abstract class ElementX extends Element with ElementCommon {
String _fixedBackendName = null;
bool _isNative = false;
bool get isNative => _isNative;
bool get hasFixedBackendName => _fixedBackendName != null;
String get fixedBackendName => _fixedBackendName;
String _jsInteropName = null;
bool _isJsInterop = false;
/// Whether the element is implemented via typed JavaScript interop.
bool get isJsInterop => _isJsInterop;
/// JavaScript name for the element if it is implemented via typed JavaScript
/// interop.
String get jsInteropName => _jsInteropName;
void markAsJsInterop() {
_isJsInterop = true;
}
void setJsInteropName(String name) {
assert(invariant(this,
_isJsInterop,
message: 'Element is not js interop but given a js interop name.'));
_jsInteropName = name;
}
/// Whether the element corresponds to a native JavaScript construct either
/// through the existing [setNative] mechanism which is only allowed
/// for internal libraries or via the new typed JavaScriptInterop mechanism
/// which is allowed for user libraries.
bool get isNative => _isNative || isJsInterop;
bool get hasFixedBackendName => fixedBackendName != null || isJsInterop;
String _jsNameHelper(Element e) {
assert(invariant(this,
!(_isJsInterop && _jsInteropName == null),
message:
'Element is js interop but js interop name has not yet been'
'computed.'));
if (e.jsInteropName != null && e.jsInteropName.isNotEmpty) {
return e.jsInteropName;
}
return e.isLibrary ? 'self' : e.name;
}
String get fixedBackendName {
if (_fixedBackendName == null && isJsInterop) {
// If an element isJsInterop but _isJsInterop is false that means it is
// considered interop as the parent class is interop.
_fixedBackendName = _jsNameHelper(isConstructor ? enclosingClass : this);
}
return _fixedBackendName;
}
// Marks this element as a native element.
void setNative(String name) {
_isNative = true;
@ -2008,6 +2053,18 @@ abstract class BaseFunctionElementX
typeCache = _functionSignatureCache.type;
}
/// An function is part of JsInterop in the following cases:
/// * It has a jsInteropName annotation
/// * It is external member of a class or library tagged as JsInterop.
bool get isJsInterop {
if (!isExternal) return false;
if (super.isJsInterop) return true;
if (isClassMember) return contextClass.isJsInterop;
if (isTopLevel) return library.isJsInterop;
return false;
}
List<ParameterElement> get parameters {
// TODO(johnniwinther): Store the list directly, possibly by using List
// instead of Link in FunctionSignature.
@ -2628,7 +2685,7 @@ abstract class BaseClassElementX extends ElementX
return asInstanceOf(compiler.functionClass) != null || callType != null;
}
bool get isNative => nativeTagInfo != null;
bool get isNative => nativeTagInfo != null || isJsInterop;
void setNative(String name) {
// TODO(johnniwinther): Assert that this is only called once. The memory

View file

@ -415,7 +415,8 @@ class MemberTypeInformation extends ElementTypeInformation
} else {
assert(element.isFunction ||
element.isGetter ||
element.isSetter);
element.isSetter ||
element.isConstructor);
TypedElement typedElement = element;
var elementType = typedElement.type;
if (elementType.kind != TypeKind.FUNCTION) {

View file

@ -233,6 +233,8 @@ class JavaScriptBackend extends Backend {
new Uri(scheme: 'dart', path: '_js_embedded_names');
static final Uri DART_ISOLATE_HELPER =
new Uri(scheme: 'dart', path: '_isolate_helper');
static final Uri PACKAGE_JS =
new Uri(scheme: 'package', path: 'js/js.dart');
static final Uri PACKAGE_LOOKUP_MAP =
new Uri(scheme: 'package', path: 'lookup_map/lookup_map.dart');
@ -291,6 +293,8 @@ class JavaScriptBackend extends Backend {
ClassElement jsBoolClass;
ClassElement jsPlainJavaScriptObjectClass;
ClassElement jsUnknownJavaScriptObjectClass;
ClassElement jsJavaScriptFunctionClass;
ClassElement jsJavaScriptObjectClass;
ClassElement jsIndexableClass;
ClassElement jsMutableIndexableClass;
@ -327,6 +331,8 @@ class JavaScriptBackend extends Backend {
ClassElement forceInlineClass;
ClassElement irRepresentationClass;
ClassElement jsAnnotationClass;
Element getInterceptorMethod;
ClassElement jsInvocationMirrorClass;
@ -616,6 +622,9 @@ class JavaScriptBackend extends Backend {
/// Codegen support for tree-shaking entries of `LookupMap`.
LookupMapAnalysis lookupMapAnalysis;
/// Codegen support for typed JavaScript interop.
JsInteropAnalysis jsInteropAnalysis;
/// Support for classifying `noSuchMethod` implementations.
NoSuchMethodRegistry noSuchMethodRegistry;
@ -656,6 +665,8 @@ class JavaScriptBackend extends Backend {
typeVariableHandler = new TypeVariableHandler(compiler);
customElementsAnalysis = new CustomElementsAnalysis(this);
lookupMapAnalysis = new LookupMapAnalysis(this, reporter);
jsInteropAnalysis = new JsInteropAnalysis(this);
noSuchMethodRegistry = new NoSuchMethodRegistry(this);
constantCompilerTask = new JavaScriptConstantTask(compiler);
resolutionCallbacks = new JavaScriptResolutionCallbacks(this);
@ -679,7 +690,7 @@ class JavaScriptBackend extends Backend {
}
FunctionElement resolveExternalFunction(FunctionElement element) {
if (isForeign(element)) return element;
if (isForeign(element) || element.isJsInterop) return element;
return patchResolverTask.measure(() {
return patchResolverTask.resolveExternalFunction(element);
});
@ -1082,7 +1093,9 @@ class JavaScriptBackend extends Backend {
} else if (Elements.isNativeOrExtendsNative(cls)) {
enqueue(enqueuer, getNativeInterceptorMethod, registry);
enqueueClass(enqueuer, jsInterceptorClass, compiler.globalDependencies);
enqueueClass(enqueuer, jsJavaScriptObjectClass, registry);
enqueueClass(enqueuer, jsPlainJavaScriptObjectClass, registry);
enqueueClass(enqueuer, jsJavaScriptFunctionClass, registry);
} else if (cls == mapLiteralClass) {
// For map literals, the dependency between the implementation class
// and [Map] is not visible, so we have to add it manually.
@ -1159,10 +1172,14 @@ class JavaScriptBackend extends Backend {
addInterceptors(jsUInt31Class, enqueuer, registry);
addInterceptors(jsDoubleClass, enqueuer, registry);
addInterceptors(jsNumberClass, enqueuer, registry);
} else if (cls == jsJavaScriptObjectClass) {
addInterceptors(jsJavaScriptObjectClass, enqueuer, registry);
} else if (cls == jsPlainJavaScriptObjectClass) {
addInterceptors(jsPlainJavaScriptObjectClass, enqueuer, registry);
} else if (cls == jsUnknownJavaScriptObjectClass) {
addInterceptors(jsUnknownJavaScriptObjectClass, enqueuer, registry);
} else if (cls == jsJavaScriptFunctionClass) {
addInterceptors(jsJavaScriptFunctionClass, enqueuer, registry);
} else if (Elements.isNativeOrExtendsNative(cls)) {
addInterceptorsForNativeClassMembers(cls, enqueuer);
} else if (cls == jsIndexingBehaviorInterface) {
@ -1192,7 +1209,9 @@ class JavaScriptBackend extends Backend {
if (!enqueuer.nativeEnqueuer.hasInstantiatedNativeClasses()) return;
Registry registry = compiler.globalDependencies;
enqueue(enqueuer, getNativeInterceptorMethod, registry);
enqueueClass(enqueuer, jsJavaScriptObjectClass, registry);
enqueueClass(enqueuer, jsPlainJavaScriptObjectClass, registry);
enqueueClass(enqueuer, jsJavaScriptFunctionClass, registry);
needToInitializeIsolateAffinityTag = true;
needToInitializeDispatchProperty = true;
}
@ -1584,6 +1603,7 @@ class JavaScriptBackend extends Backend {
}
ClassElement defaultSuperclass(ClassElement element) {
if (element.isJsInterop) return jsJavaScriptObjectClass;
// Native classes inherit from Interceptor.
return element.isNative ? jsInterceptorClass : compiler.objectClass;
}
@ -2000,6 +2020,8 @@ class JavaScriptBackend extends Backend {
jsExtendableArrayClass = findClass('JSExtendableArray');
jsUnmodifiableArrayClass = findClass('JSUnmodifiableArray');
jsPlainJavaScriptObjectClass = findClass('PlainJavaScriptObject');
jsJavaScriptObjectClass = findClass('JavaScriptObject');
jsJavaScriptFunctionClass = findClass('JavaScriptFunction');
jsUnknownJavaScriptObjectClass = findClass('UnknownJavaScriptObject');
jsIndexableClass = findClass('JSIndexable');
jsMutableIndexableClass = findClass('JSMutableIndexable');
@ -2042,6 +2064,8 @@ class JavaScriptBackend extends Backend {
} else if (uri == Uris.dart__native_typed_data) {
typedArrayClass = findClass('NativeTypedArray');
typedArrayOfIntClass = findClass('NativeTypedArrayOfInt');
} else if (uri == PACKAGE_JS) {
jsAnnotationClass = find(library, 'Js');
}
annotations.onLibraryScanned(library);
});
@ -2496,6 +2520,7 @@ class JavaScriptBackend extends Backend {
void onQueueClosed() {
lookupMapAnalysis.onQueueClosed();
jsInteropAnalysis.onQueueClosed();
}
void onCodegenStart() {

View file

@ -71,6 +71,9 @@ class CustomElementsAnalysis {
if (!Elements.isNativeOrExtendsNative(classElement)) return;
if (classElement.isMixinApplication) return;
if (classElement.isAbstract) return;
// JsInterop classes are opaque interfaces without a concrete
// implementation.
if (classElement.isJsInterop) return;
joinFor(enqueuer).instantiatedClasses.add(classElement);
}

View file

@ -72,6 +72,8 @@ import '../js_emitter/js_emitter.dart' show
USE_LAZY_EMITTER;
import '../library_loader.dart' show LibraryLoader, LoadedLibraries;
import '../native/native.dart' as native;
import '../patch_parser.dart' show
checkJsInteropAnnotation;
import '../resolution/registry.dart' show
EagerRegistry;
import '../resolution/tree_elements.dart' show
@ -95,6 +97,7 @@ import 'backend_impact.dart';
import 'codegen/task.dart';
import 'constant_system_javascript.dart';
import 'patch_resolver.dart';
import 'js_interop_analysis.dart' show JsInteropAnalysis;
import 'lookup_map_analysis.dart' show LookupMapAnalysis;
part 'backend.dart';

View file

@ -0,0 +1,142 @@
// Copyright (c) 2015, 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.
/// Analysis to determine how to generate code for typed JavaScript interop.
library compiler.src.js_backend.js_interop_analysis;
import '../common/names.dart' show Identifiers;
import '../compiler.dart' show Compiler;
import '../diagnostics/messages.dart' show MessageKind;
import '../constants/values.dart'
show
ConstantValue,
ConstructedConstantValue,
ListConstantValue,
NullConstantValue,
StringConstantValue,
TypeConstantValue;
import '../elements/elements.dart'
show
ClassElement,
Element,
FieldElement,
FunctionElement,
LibraryElement,
MetadataAnnotation;
import '../js/js.dart' as jsAst;
import '../js/js.dart' show js;
import '../universe/selector.dart' show Selector;
import '../universe/universe.dart' show SelectorConstraints;
import 'js_backend.dart' show JavaScriptBackend;
class JsInteropAnalysis {
final JavaScriptBackend backend;
/// The resolved [FieldElement] for `Js.name`.
FieldElement nameField;
bool enabledJsInterop = false;
/// Whether the backend is currently processing the codegen queue.
bool _inCodegen = false;
JsInteropAnalysis(this.backend);
void onQueueClosed() {
if (_inCodegen) return;
if (backend.jsAnnotationClass != null) {
nameField = backend.jsAnnotationClass.lookupMember('name');
backend.compiler.libraryLoader.libraries
.forEach(processJsInteropAnnotationsInLibrary);
}
}
void onCodegenStart() {
_inCodegen = true;
}
void processJsInteropAnnotation(Element e) {
for (MetadataAnnotation annotation in e.implementation.metadata) {
ConstantValue constant = backend.compiler.constants.getConstantValue(
annotation.constant);
if (constant == null || constant is! ConstructedConstantValue) continue;
ConstructedConstantValue constructedConstant = constant;
if (constructedConstant.type.element == backend.jsAnnotationClass) {
ConstantValue value = constructedConstant.fields[nameField];
if (value.isString) {
StringConstantValue stringValue = value;
e.setJsInteropName(stringValue.primitiveValue.slowToString());
} else {
// TODO(jacobr): report a warning if the value is not a String.
e.setJsInteropName('');
}
enabledJsInterop = true;
return;
}
}
}
void processJsInteropAnnotationsInLibrary(LibraryElement library) {
processJsInteropAnnotation(library);
library.implementation.forEachLocalMember((Element element) {
processJsInteropAnnotation(element);
if (!element.isClass || !element.isJsInterop) return;
ClassElement classElement = element;
if (!classElement
.implementsInterface(backend.jsJavaScriptObjectClass)) {
backend.reporter.reportErrorMessage(classElement,
MessageKind.JS_INTEROP_CLASS_CANNOT_EXTEND_DART_CLASS, {
'cls': classElement.name,
'superclass': classElement.superclass.name
});
}
classElement.forEachMember(
(ClassElement classElement, Element member) {
processJsInteropAnnotation(member);
if (!member.isSynthesized &&
classElement.isJsInterop &&
member is FunctionElement) {
FunctionElement fn = member;
if (!fn.isExternal && !fn.isAbstract) {
backend.reporter.reportErrorMessage(
fn,
MessageKind.JS_INTEROP_CLASS_NON_EXTERNAL_MEMBER,
{'cls': classElement.name, 'member': member.name});
}
}
});
});
}
jsAst.Statement buildJsInteropBootstrap() {
if (!enabledJsInterop) return null;
List<jsAst.Statement> statements = <jsAst.Statement>[];
backend.compiler.codegenWorld.forEachInvokedName(
(String name, Map<Selector, SelectorConstraints> selectors) {
selectors.forEach((Selector selector, SelectorConstraints constraints) {
if (selector.isClosureCall) {
// TODO(jacobr): support named arguments.
if (selector.namedArgumentCount > 0) return;
int argumentCount = selector.argumentCount;
var candidateParameterNames =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLOMOPQRSTUVWXYZ';
var parameters = new List<String>.generate(
argumentCount, (i) => candidateParameterNames[i]);
var name = backend.namer.invocationName(selector);
statements.add(js.statement(
'Function.prototype.# = function(#) { return this(#) }',
[name, parameters, parameters]));
}
});
});
return new jsAst.Block(statements);
}
}

View file

@ -636,6 +636,32 @@ class Namer {
return invocationName(new Selector.fromElement(method));
}
String _jsNameHelper(Element e) {
if (e.jsInteropName != null && e.jsInteropName.isNotEmpty)
return e.jsInteropName;
return e.isLibrary ? 'self' : e.name;
}
/// Returns a JavaScript path specifying the context in which
/// [element.fixedBackendName] should be evaluated. Only applicable for
/// elements using typed JavaScript interop.
/// For example: fixedBackendPath for the static method createMap in the
/// Map class of the goog.map JavaScript library would have path
/// "goog.maps.Map".
String fixedBackendPath(Element element) {
if (!element.isJsInterop) return null;
if (element.isInstanceMember) return 'this';
if (element.isConstructor) return fixedBackendPath(element.enclosingClass);
if (element.isLibrary) return 'self';
var sb = new StringBuffer();
sb..write(_jsNameHelper(element.library));
if (element.enclosingClass != null && element.enclosingClass != element) {
sb..write('.')..write(_jsNameHelper(element.enclosingClass));
}
return sb.toString();
}
/// Returns the annotated name for a variant of `call`.
/// The result has the form:
///
@ -766,11 +792,6 @@ class Namer {
ClassElement enclosingClass = element.enclosingClass;
if (element.hasFixedBackendName) {
// Certain native fields must be given a specific name. Native names must
// not contain '$'. We rely on this to avoid clashes.
assert(enclosingClass.isNative &&
!element.fixedBackendName.contains(r'$'));
return new StringBackedName(element.fixedBackendName);
}

View file

@ -31,7 +31,7 @@ class PatchResolverTask extends CompilerTask {
});
checkMatchingPatchSignatures(element, patch);
element = patch;
} else {
} else if (!element.isJsInterop) {
reporter.reportErrorMessage(
element, MessageKind.PATCH_EXTERNAL_WITHOUT_IMPLEMENTATION);
}

View file

@ -127,6 +127,8 @@ jsAst.Statement buildSetupProgram(Program program, Compiler compiler,
'finishedClassesAccess': finishedClassesAccess,
'needsMixinSupport': emitter.needsMixinSupport,
'needsNativeSupport': program.needsNativeSupport,
'enabledJsInterop': backend.jsInteropAnalysis.enabledJsInterop,
'jsInteropBoostrap':backend.jsInteropAnalysis.buildJsInteropBootstrap(),
'isInterceptorClass': namer.operatorIs(backend.jsInterceptorClass),
'isObject' : namer.operatorIs(compiler.objectClass),
'specProperty': js.string(namer.nativeSpecProperty),
@ -142,7 +144,6 @@ jsAst.Statement buildSetupProgram(Program program, Compiler compiler,
'nativeInfoHandler': nativeInfoHandler,
'operatorIsPrefix' : js.string(namer.operatorIsPrefix),
'deferredActionString': js.string(namer.deferredAction)};
String skeleton = '''
function $setupProgramName(programData, typesOffset) {
"use strict";
@ -754,6 +755,9 @@ function $setupProgramName(programData, typesOffset) {
}
}
if (#enabledJsInterop) {
#jsInteropBoostrap
}
#tearOffCode;
}

View file

@ -140,10 +140,14 @@ class InterceptorStubGenerator {
if (hasNative) {
statements.add(js.statement(r'''{
if (typeof receiver != "object") return receiver;
if (typeof receiver != "object") {
if (typeof receiver == "function" ) return #;
return receiver;
}
if (receiver instanceof #) return receiver;
return #(receiver);
}''', [
interceptorFor(backend.jsJavaScriptFunctionClass),
backend.emitter.constructorAccess(compiler.objectClass),
backend.emitter
.staticFunctionAccess(backend.getNativeInterceptorMethod)]));

View file

@ -134,8 +134,9 @@ class NativeEmitter {
} else if (extensionPoints.containsKey(cls)) {
needed = true;
}
if (cls.isNative &&
native.nativeTagsForcedNonLeaf(classElement)) {
if (classElement.isJsInterop) {
needed = true; // TODO(jacobr): we don't need all interop classes.
} else if (cls.isNative && native.nativeTagsForcedNonLeaf(classElement)) {
needed = true;
nonLeafClasses.add(cls);
}
@ -154,6 +155,7 @@ class NativeEmitter {
for (Class cls in classes) {
if (!cls.isNative) continue;
if (cls.element.isJsInterop) continue;
List<String> nativeTags = native.nativeTagsOfClass(cls.element);
if (nonLeafClasses.contains(cls) ||
@ -294,7 +296,7 @@ class NativeEmitter {
// The target JS function may check arguments.length so we need to
// make sure not to pass any unspecified optional arguments to it.
// For example, for the following Dart method:
// foo([x, y, z]);
// foo({x, y, z});
// The call:
// foo(y: 1)
// must be turned into a JS call to:
@ -319,9 +321,20 @@ class NativeEmitter {
} else {
// Native methods that are not intercepted must be static.
assert(invariant(member, member.isStatic));
receiver = js('this');
arguments = argumentsBuffer.sublist(0,
indexOfLastOptionalArgumentInParameters + 1);
if (member.isJsInterop) {
// fixedBackendPath is allowed to have the form foo.bar.baz for
// interop. This template is uncached to avoid possibly running out of
// memory when Dart2Js is run in server mode. In reality the risk of
// caching these templates causing an issue is very low as each class
// and library that uses typed JavaScript interop will create only 1
// unique template.
receiver = js.uncachedExpressionTemplate(
backend.namer.fixedBackendPath(member)).instantiate([]);
} else {
receiver = js('this');
}
}
statements.add(
js.statement('return #.#(#)', [receiver, target, arguments]));

View file

@ -37,6 +37,7 @@ import '../../elements/elements.dart' show
FunctionSignature,
LibraryElement,
MethodElement,
Name,
ParameterElement,
TypedefElement,
VariableElement;
@ -44,7 +45,10 @@ import '../../js/js.dart' as js;
import '../../js_backend/js_backend.dart' show
Namer,
JavaScriptBackend,
JavaScriptConstantCompiler;
JavaScriptConstantCompiler,
StringBackedName;
import '../../universe/call_structure.dart' show
CallStructure;
import '../../universe/selector.dart' show
Selector;
import '../../universe/universe.dart' show
@ -163,6 +167,8 @@ class ProgramBuilder {
nativeClasses, interceptorClassesNeededByConstants,
classesModifiedByEmitRTISupport);
_addJsInteropStubs(_registry.mainLibrariesMap);
MainFragment mainFragment = _buildMainFragment(_registry.mainLibrariesMap);
Iterable<Fragment> deferredFragments =
_registry.deferredLibrariesMap.map(_buildDeferredFragment);
@ -340,6 +346,99 @@ class ProgramBuilder {
return libraries;
}
void _addJsInteropStubs(LibrariesMap librariesMap) {
if (_classes.containsKey(_compiler.objectClass)) {
var toStringInvocation = namer.invocationName(new Selector.call(
new Name("toString", _compiler.objectClass.library),
CallStructure.NO_ARGS));
// TODO(jacobr): register toString as used so that it is always accessible
// from JavaScript.
_classes[_compiler.objectClass].callStubs.add(_buildStubMethod(
new StringBackedName("toString"),
js.js('function() { return this.#(this) }', toStringInvocation)));
}
// We add all members from classes marked with isJsInterop to the base
// Interceptor class with implementations that directly call the
// corresponding JavaScript member. We do not attempt to bind this when
// tearing off JavaScript methods as we cannot distinguish between calling
// a regular getter that returns a JavaScript function and tearing off
// a method in the case where there exist multiple JavaScript classes
// that conflict on whether the member is a getter or a method.
var interceptorClass = _classes[backend.jsJavaScriptObjectClass];
var stubNames = new Set<String>();
librariesMap.forEach((LibraryElement library, List<Element> elements) {
for (Element e in elements) {
if (e is ClassElement && e.isJsInterop) {
e.declaration.forEachMember((_, Element member) {
if (!member.isInstanceMember) return;
if (member.isGetter || member.isField || member.isFunction) {
var selectors =
_compiler.codegenWorld.getterInvocationsByName(member.name);
if (selectors != null && !selectors.isEmpty) {
for (var selector in selectors.keys) {
var stubName = namer.invocationName(selector);
if (stubNames.add(stubName.key)) {
interceptorClass.callStubs.add(_buildStubMethod(
stubName,
js.js(
'function(obj) { return obj.# }', [member.name]),
element: member));
}
}
}
}
if (member.isSetter || (member.isField && !member.isConst)) {
var selectors =
_compiler.codegenWorld.setterInvocationsByName(member.name);
if (selectors != null && !selectors.isEmpty) {
var stubName = namer.setterForElement(member);
if (stubNames.add(stubName.key)) {
interceptorClass.callStubs.add(_buildStubMethod(
stubName,
js.js('function(obj, v) { return obj.# = v }',
[member.name]),
element: member));
}
}
}
if (member.isFunction) {
var selectors =
_compiler.codegenWorld.invocationsByName(member.name);
FunctionElement fn = member;
// Named arguments are not yet supported. In the future we
// may want to map named arguments to an object literal containing
// all named arguments.
if (selectors != null && !selectors.isEmpty) {
for (var selector in selectors.keys) {
// Check whether the arity matches this member.
var argumentCount = selector.argumentCount;
if (argumentCount > fn.parameters.length) break;
if (argumentCount < fn.parameters.length &&
!fn.parameters[argumentCount].isOptional) break;
var stubName = namer.invocationName(selector);
if (!stubNames.add(stubName.key)) break;
var candidateParameterNames =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLOMOPQRSTUVWXYZ';
var parameters = new List<String>.generate(argumentCount,
(i) => candidateParameterNames[i]);
interceptorClass.callStubs.add(_buildStubMethod(
stubName,
js.js('function(receiver, #) { return receiver.#(#) }',
[parameters, member.name, parameters]),
element: member));
}
}
}
});
}
}
});
}
// Note that a library-element may have multiple [Library]s, if it is split
// into multiple output units.
Library _buildLibrary(LibraryElement library, List<Element> elements) {
@ -387,6 +486,11 @@ class ProgramBuilder {
Class _buildClass(ClassElement element) {
bool onlyForRti = collector.classesOnlyNeededForRti.contains(element);
if (element.isJsInterop) {
// TODO(jacobr): check whether the class has any active static fields
// if it does not we can suppress it completely.
onlyForRti = true;
}
List<Method> methods = [];
List<StubMethod> callStubs = <StubMethod>[];
@ -467,28 +571,35 @@ class ProgramBuilder {
storeFunctionTypeInMetadata: _storeFunctionTypesInMetadata);
List<StubMethod> checkedSetters = <StubMethod>[];
for (Field field in instanceFields) {
if (field.needsCheckedSetter) {
assert(!field.needsUncheckedSetter);
Element element = field.element;
js.Expression code = backend.generatedCode[element];
assert(code != null);
js.Name name = namer.deriveSetterName(field.accessorName);
checkedSetters.add(_buildStubMethod(name, code, element: element));
}
}
List<StubMethod> isChecks = <StubMethod>[];
typeTests.properties.forEach((js.Name name, js.Node code) {
isChecks.add(_buildStubMethod(name, code));
});
if (element.isJsInterop) {
typeTests.properties.forEach((js.Name name, js.Node code) {
_classes[backend.jsInterceptorClass].isChecks.add(
_buildStubMethod(name, code));
});
} else {
for (Field field in instanceFields) {
if (field.needsCheckedSetter) {
assert(!field.needsUncheckedSetter);
Element element = field.element;
js.Expression code = backend.generatedCode[element];
assert(code != null);
js.Name name = namer.deriveSetterName(field.accessorName);
checkedSetters.add(_buildStubMethod(name, code, element: element));
}
}
typeTests.properties.forEach((js.Name name, js.Node code) {
isChecks.add(_buildStubMethod(name, code));
});
}
js.Name name = namer.className(element);
String holderName = namer.globalObjectFor(element);
// TODO(floitsch): we shouldn't update the registry in the middle of
// building a class.
Holder holder = _registry.registerHolder(holderName);
bool isInstantiated =
bool isInstantiated = !element.isJsInterop &&
_compiler.codegenWorld.directlyInstantiatedClasses.contains(element);
Class result;

View file

@ -68,7 +68,7 @@ class RuntimeTypeGenerator {
/// native classes.
/// TODO(herhut): Generate tests for native classes dynamically, as well.
void generateIsTest(Element other) {
if (classElement.isNative ||
if (classElement.isJsInterop || classElement.isNative ||
!classElement.isSubclassOf(other)) {
result.properties[namer.operatorIs(other)] = js('1');
}

View file

@ -544,6 +544,8 @@ class NativeResolutionEnqueuer extends NativeEnqueuerBase {
void processNativeClass(ClassElement classElement) {
super.processNativeClass(classElement);
// Js Interop interfaces do not have tags.
if (classElement.isJsInterop) return;
// Since we map from dispatch tags to classes, a dispatch tag must be used
// on only one native class.
for (String tag in nativeTagsOfClass(classElement)) {

View file

@ -133,6 +133,8 @@ import 'elements/modelx.dart' show
LibraryElementX,
MetadataAnnotationX,
SetterElementX;
import 'js_backend/js_backend.dart' show
JavaScriptBackend;
import 'library_loader.dart' show
LibraryLoader;
import 'parser/listener.dart' show
@ -304,6 +306,7 @@ void patchElement(Compiler compiler,
patch, MessageKind.PATCH_NON_EXISTING, {'name': patch.name});
return;
}
if (!(origin.isClass ||
origin.isConstructor ||
origin.isFunction ||
@ -368,6 +371,12 @@ checkNativeAnnotation(Compiler compiler, ClassElement cls) {
const NativeAnnotationHandler());
}
checkJsInteropAnnotation(Compiler compiler, element) {
EagerAnnotationHandler.checkAnnotation(compiler, element,
const JsInteropAnnotationHandler());
}
/// Abstract interface for pre-resolution detection of metadata.
///
/// The detection is handled in two steps:
@ -456,6 +465,39 @@ class NativeAnnotationHandler implements EagerAnnotationHandler<String> {
}
}
/// Annotation handler for pre-resolution detection of `@Js(...)`
/// annotations.
class JsInteropAnnotationHandler implements EagerAnnotationHandler<bool> {
const JsInteropAnnotationHandler();
bool hasJsNameAnnotation(MetadataAnnotation annotation) =>
annotation.beginToken != null && annotation.beginToken.next.value == 'Js';
bool apply(Compiler compiler,
Element element,
MetadataAnnotation annotation) {
bool hasJsInterop = hasJsNameAnnotation(annotation);
if (hasJsInterop) {
element.markAsJsInterop();
}
// Due to semantics of apply in the baseclass we have to return null to
// indicate that no match was found.
return hasJsInterop ? true : null;
}
@override
void validate(Compiler compiler,
Element element,
MetadataAnnotation annotation,
ConstantValue constant) {
JavaScriptBackend backend = compiler.backend;
if (constant.getType(compiler.coreTypes).element !=
backend.jsAnnotationClass) {
compiler.reporter.internalError(annotation, 'Invalid @Js(...) annotation.');
}
}
}
/// Annotation handler for pre-resolution detection of `@patch` annotations.
class PatchAnnotationHandler implements EagerAnnotationHandler<PatchVersion> {
const PatchAnnotationHandler();

View file

@ -136,6 +136,12 @@ abstract class ElementZ extends Element with ElementCommon {
@override
bool get isNative => false;
@override
bool get isJsInterop => false;
@override
String get jsInteropName => null;
@override
bool get isOperator => false;

View file

@ -1339,6 +1339,11 @@ class SsaBuilder extends ast.Visitor
// enqueued.
backend.registerStaticUse(element, compiler.enqueuer.codegen);
if (element.isJsInterop && !element.isFactoryConstructor) {
// We only inline factory JavaScript interop constructors.
return false;
}
// Ensure that [element] is an implementation element.
element = element.implementation;
@ -1369,6 +1374,8 @@ class SsaBuilder extends ast.Visitor
}
}
if (element.isJsInterop) return false;
// Don't inline operator== methods if the parameter can be null.
if (element.name == '==') {
if (element.enclosingClass != compiler.objectClass
@ -1547,6 +1554,13 @@ class SsaBuilder extends ast.Visitor
});
}
/**
* Return null so it is simple to remove the optional parameters completely
* from interop methods to match JavaScript semantics for ommitted arguments.
*/
HInstruction handleConstantForOptionalParameterJsInterop(Element parameter) =>
null;
HInstruction handleConstantForOptionalParameter(Element parameter) {
ConstantValue constantValue =
backend.constants.getConstantValueForVariable(parameter);
@ -1634,10 +1648,19 @@ class SsaBuilder extends ast.Visitor
graph.calledInLoop = compiler.world.isCalledInLoop(functionElement);
ast.FunctionExpression function = functionElement.node;
assert(function != null);
assert(invariant(functionElement, !function.modifiers.isExternal));
assert(elements.getFunctionDefinition(function) != null);
openFunction(functionElement, function);
String name = functionElement.name;
if (functionElement.isJsInterop) {
push(invokeJsInteropFunction(functionElement, parameters.values.toList(),
sourceInformationBuilder.buildGeneric(function)));
var value = pop();
closeAndGotoExit(new HReturn(value,
sourceInformationBuilder.buildReturn(functionElement.node)));
return closeFunction();
}
assert(invariant(functionElement, !function.modifiers.isExternal));
// If [functionElement] is `operator==` we explicitely add a null check at
// the beginning of the method. This is to avoid having call sites do the
// null check.
@ -1842,6 +1865,7 @@ class SsaBuilder extends ast.Visitor
*/
void visitInlinedFunction(FunctionElement function) {
potentiallyCheckInlinedParameterTypes(function);
if (function.isGenerativeConstructor) {
buildFactory(function);
} else {
@ -2145,7 +2169,8 @@ class SsaBuilder extends ast.Visitor
ClassElement classElement =
functionElement.enclosingClass.implementation;
bool isNativeUpgradeFactory =
Elements.isNativeOrExtendsNative(classElement);
Elements.isNativeOrExtendsNative(classElement)
&& !classElement.isJsInterop;
ast.FunctionExpression function = functionElement.node;
// Note that constructors (like any other static function) do not need
// to deal with optional arguments. It is the callers job to provide all
@ -3956,7 +3981,9 @@ class SsaBuilder extends ast.Visitor
arguments,
element,
compileArgument,
handleConstantForOptionalParameter);
element.isJsInterop ?
handleConstantForOptionalParameterJsInterop :
handleConstantForOptionalParameter);
}
void addGenericSendArgumentsToList(Link<ast.Node> link, List<HInstruction> list) {
@ -5101,7 +5128,8 @@ class SsaBuilder extends ast.Visitor
var inputs = <HInstruction>[];
if (constructor.isGenerativeConstructor &&
Elements.isNativeOrExtendsNative(constructor.enclosingClass)) {
Elements.isNativeOrExtendsNative(constructor.enclosingClass) &&
!constructor.isJsInterop) {
// Native class generative constructors take a pre-constructed object.
inputs.add(graph.addConstantNull(compiler));
}
@ -5818,6 +5846,96 @@ class SsaBuilder extends ast.Visitor
}
}
HForeignCode invokeJsInteropFunction(Element element,
List<HInstruction> arguments,
SourceInformation sourceInformation) {
assert(element.isJsInterop);
nativeEmitter.nativeMethods.add(element);
String templateString;
if (element.isFactoryConstructor) {
// Treat factory constructors as syntactic sugar for creating object
// literals.
ConstructorElement constructor = element;
FunctionSignature params = constructor.functionSignature;
int i = 0;
int positions = 0;
var filteredArguments = <HInstruction>[];
var parameterNameMap = new Map<String, js.Expression>();
params.orderedForEachParameter((ParameterElement parameter) {
// TODO(jacobr): throw if parameter names do not match names of property
// names in the class.
assert (parameter.isNamed);
if (!parameter.isNamed) {
reporter.reportErrorMessage(
parameter, MessageKind.GENERIC,
{'text': 'All arguments to external constructors of JavaScript '
'interop classes must be named as these constructors '
'are syntactic sugar for object literals.'});
}
HInstruction argument = arguments[i];
if (argument != null) {
filteredArguments.add(argument);
parameterNameMap[parameter.name] =
new js.InterpolatedExpression(positions++);
}
i++;
});
var codeTemplate = new js.Template(null,
js.objectLiteral(parameterNameMap));
var nativeBehavior = new native.NativeBehavior()
..codeTemplate = codeTemplate;
return new HForeignCode(
codeTemplate,
backend.dynamicType, filteredArguments,
nativeBehavior: nativeBehavior)
..sourceInformation = sourceInformation;
}
var target = new HForeignCode(js.js.parseForeignJS(
"${backend.namer.fixedBackendPath(element)}."
"${element.fixedBackendName}"),
backend.dynamicType,
<HInstruction>[]);
add(target);
// Strip off trailing arguments that were not specified.
// we could assert that the trailing arguments are all null.
// TODO(jacobr): rewrite named arguments to an object literal matching
// the factory constructor case.
arguments = arguments.where((arg) => arg != null).toList();
var inputs = <HInstruction>[target]..addAll(arguments);
js.Template codeTemplate;
if (element.isGetter) {
codeTemplate = js.js.parseForeignJS("#");
} else if (element.isSetter) {
codeTemplate = js.js.parseForeignJS("# = #");
} else {
var argsStub = <String>[];
for (int i = 0; i < arguments.length; i++) {
argsStub.add('#');
}
if (element.isConstructor) {
codeTemplate = js.js.parseForeignJS("new #(${argsStub.join(",")})");
} else {
codeTemplate = js.js.parseForeignJS("#(${argsStub.join(",")})");
}
}
var nativeBehavior = new native.NativeBehavior()
..codeTemplate = codeTemplate
..typesReturned.add(
backend.jsJavaScriptObjectClass.thisType)
..typesInstantiated.add(
backend.jsJavaScriptObjectClass.thisType)
..sideEffects.setAllSideEffects();
return new HForeignCode(
codeTemplate,
backend.dynamicType, inputs,
nativeBehavior: nativeBehavior)
..sourceInformation = sourceInformation;
}
void pushInvokeStatic(ast.Node location,
Element element,
List<HInstruction> arguments,
@ -5836,16 +5954,22 @@ class SsaBuilder extends ast.Visitor
}
bool targetCanThrow = !compiler.world.getCannotThrow(element);
// TODO(5346): Try to avoid the need for calling [declaration] before
// creating an [HInvokeStatic].
HInvokeStatic instruction = new HInvokeStatic(
element.declaration, arguments, typeMask,
targetCanThrow: targetCanThrow)
..sourceInformation = sourceInformation;
if (!currentInlinedInstantiations.isEmpty) {
instruction.instantiatedTypes = new List<DartType>.from(
currentInlinedInstantiations);
var instruction;
if (element.isJsInterop) {
instruction = invokeJsInteropFunction(element, arguments,
sourceInformation);
} else {
// creating an [HInvokeStatic].
instruction = new HInvokeStatic(
element.declaration, arguments, typeMask,
targetCanThrow: targetCanThrow)
..sourceInformation = sourceInformation;
if (!currentInlinedInstantiations.isEmpty) {
instruction.instantiatedTypes = new List<DartType>.from(
currentInlinedInstantiations);
}
instruction.sideEffects = compiler.world.getSideEffectsOfElement(element);
}
instruction.sideEffects = compiler.world.getSideEffectsOfElement(element);
if (location == null) {
push(instruction);
} else {

View file

@ -286,6 +286,10 @@ class TypesTask extends CompilerTask {
*/
TypeMask getGuaranteedTypeOfElement(Element element) {
return measure(() {
// TODO(24489): trust some JsInterop types.
if (element.isJsInterop) {
return dynamicType;
}
TypeMask guaranteedType = typesInferrer.getTypeOfElement(element);
return guaranteedType;
});
@ -293,6 +297,11 @@ class TypesTask extends CompilerTask {
TypeMask getGuaranteedReturnTypeOfElement(Element element) {
return measure(() {
// TODO(24489): trust some JsInterop types.
if (element.isJsInterop) {
return dynamicType;
}
TypeMask guaranteedType =
typesInferrer.getReturnTypeOfElement(element);
return guaranteedType;

View file

@ -5,6 +5,8 @@ name: compiler
dependencies:
package_config: ^0.1.1
pub_semver: ^1.2.1
js:
path: ../js
js_ast:
path: ../js_ast
js_runtime:

8
pkg/js/AUTHORS Normal file
View file

@ -0,0 +1,8 @@
# Below is a list of people and organizations that have contributed
# to the Dart project. Names should be added to the list like so:
#
# Name/Organization <email address>
Google Inc.
Alexandre Ardhuin <alexandre.ardhuin@gmail.com>

24
pkg/js/LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright 2012, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

23
pkg/js/PATENTS Normal file
View file

@ -0,0 +1,23 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Dart Project.
Google hereby grants to you a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this
section) patent license to make, have made, use, offer to sell, sell,
import, transfer, and otherwise run, modify and propagate the contents
of this implementation of Dart, where such license applies only to
those patent claims, both currently owned by Google and acquired in
the future, licensable by Google that are necessarily infringed by
this implementation of Dart. This grant does not include claims that
would be infringed only as a consequence of further modification of
this implementation. If you or your agent or exclusive licensee
institute or order or agree to the institution of patent litigation
against any entity (including a cross-claim or counterclaim in a
lawsuit) alleging that this implementation of Dart or any code
incorporated within this implementation of Dart constitutes direct or
contributory patent infringement, or inducement of patent
infringement, then any patent rights granted to you under this License
for this implementation of Dart shall terminate as of the date such
litigation is filed.

60
pkg/js/README.md Normal file
View file

@ -0,0 +1,60 @@
Dart-JavaScript Interop
=======================
Status
------
Version 0.6.0 is a complete rewrite of package:js
The package now only contains annotations specifying the shape of the
JavaScript API to import into Dart.
The core implementation is defined directly in Dart2Js, Dartium, and DDC.
**Warning: support in Dartium and Dart2Js is still in progress.
#### Example - TODO(jacobr)
Configuration and Initialization
--------------------------------
### Adding the dependency
Add the following to your `pubspec.yaml`:
```yaml
dependencies:
js: ">=0.6.0 <0.7.0"
```
##### main.html
```html
<html>
<head>
</head>
<body>
<script type="application/dart" src="main.dart"></script>
</body>
</html>
```
##### main.dart
TODO(jacobr): example under construction.
```dart
library main;
import 'package:js/js.dart';
main() {
}
```
Contributing and Filing Bugs
----------------------------
Please file bugs and features requests on the Github issue tracker: https://github.com/dart-lang/js-interop/issues
We also love and accept community contributions, from API suggestions to pull requests. Please file an issue before beginning work so we can discuss the design and implementation. We are trying to create issues for all current and future work, so if something there intrigues you (or you need it!) join in on the discussion.
All we require is that you sign the Google Individual Contributor License Agreement https://developers.google.com/open-source/cla/individual?csw=1

68
pkg/js/lib/js.dart Normal file
View file

@ -0,0 +1,68 @@
// Copyright (c) 2013, 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.
/**
* The js library allows Dart library authors to export their APIs to JavaScript
* and to define Dart interfaces for JavaScript objects.
*/
library js;
export 'dart:js' show allowInterop, allowInteropCaptureThis;
/// A metadata annotation that indicates that a Library, Class, or member is
/// implemented directly in JavaScript. All external members of a class or
/// library with this annotation implicitly have it as well.
///
/// Specifying [name] customizes the JavaScript name to use. By default the
/// dart name is used. It is not valid to specify a custom [name] for class
/// instance members.
///
/// Example 1:
///
/// @Js('google.maps')
/// library maps;
///
/// external Map get map;
///
/// @Js("LatLng")
/// class Location {
/// external Location(num lat, num lng);
/// }
///
/// @Js()
/// class Map {
/// external Map(Location location);
/// external Location getLocation();
/// }
///
/// In this example the top level map getter will invoke the JavaScript getter
/// google.maps.map
/// Calls to the Map constructor will be translated to calls to the JavaScript
/// new google.maps.Map(location)
/// Calls to the Location constructor willbe translated to calls to the
/// JavaScript
/// new google.maps.LatLng(lat, lng)
/// because a custom JavaScript name for the Location class.
/// In general, we recommend against using custom JavaScript names whenever
/// possible as it is easier for users if the JavaScript names and Dart names
/// are consistent.
///
/// Example 2:
/// library utils;
///
/// @Js("JSON.stringify")
/// external String stringify(obj);
///
/// @Js()
/// void debugger();
///
/// In this example no custom JavaScript namespace is specified.
/// Calls to debugger map to calls to JavaScript
/// self.debugger()
/// Calls to stringify map to calls to
/// JSON.stringify(obj)
class Js {
final String name;
const Js([this.name]);
}

9
pkg/js/pubspec.yaml Normal file
View file

@ -0,0 +1,9 @@
name: js
version: 0.6.0-dev.1
authors:
- Dart Team <misc@dartlang.org>
- Alexandre Ardhuin <alexandre.ardhuin@gmail.com>
description: Access JavaScript from Dart.
homepage: https://github.com/dart-lang/js-interop
environment:
sdk: '>=1.13.0 <2.0.0'

View file

@ -21,6 +21,7 @@ import 'dart:_js_helper' show allMatchesInStringUnchecked,
checkString,
defineProperty,
diagnoseIndexError,
getIsolateAffinityTag,
getRuntimeType,
initNativeDispatch,
initNativeDispatchFlag,
@ -39,6 +40,7 @@ import 'dart:_js_helper' show allMatchesInStringUnchecked,
StringMatch,
firstMatchAfter,
NoInline;
import 'dart:_foreign_helper' show
JS,
JS_EFFECT,
@ -51,6 +53,9 @@ part 'js_array.dart';
part 'js_number.dart';
part 'js_string.dart';
final String DART_CLOSURE_PROPERTY_NAME =
getIsolateAffinityTag(r'_$dart_dartClosure');
String _symbolToString(Symbol symbol) => _symbol_dev.Symbol.getName(symbol);
_symbolMapToStringMap(Map<Symbol, dynamic> map) {
@ -169,6 +174,9 @@ getNativeInterceptor(object) {
// are 'plain' Objects. This test could be simplified and the dispatch path
// be faster if Object.prototype was pre-patched with a non-leaf dispatch
// record.
if (JS('bool', 'typeof # == "function"', object)) {
return JS_INTERCEPTOR_CONSTANT(JavaScriptFunction);
}
var proto = JS('', 'Object.getPrototypeOf(#)', object);
if (JS('bool', '# == null || # === Object.prototype', proto, proto)) {
return JS_INTERCEPTOR_CONSTANT(PlainJavaScriptObject);
@ -393,13 +401,18 @@ abstract class JSObject {
* Interceptor base class for JavaScript objects not recognized as some more
* specific native type.
*/
abstract class JavaScriptObject extends Interceptor implements JSObject {
class JavaScriptObject extends Interceptor implements JSObject {
const JavaScriptObject();
// It would be impolite to stash a property on the object.
int get hashCode => 0;
Type get runtimeType => JSObject;
/**
* Returns the result of the JavaScript objects `toString` method.
*/
String toString() => JS('String', 'String(#)', this);
}
@ -420,6 +433,19 @@ class PlainJavaScriptObject extends JavaScriptObject {
*/
class UnknownJavaScriptObject extends JavaScriptObject {
const UnknownJavaScriptObject();
String toString() => JS('String', 'String(#)', this);
}
/**
* Interceptor for JavaScript function objects and Dart functions that have
* been converted to JavaScript functions.
* These interceptor methods are not always used as the JavaScript function
* object has also been mangled to support Dart function calling conventions.
*/
class JavaScriptFunction extends JavaScriptObject implements Function {
const JavaScriptFunction();
String toString() {
var dartClosure = JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME);
return dartClosure == null ? super.toString() : dartClosure.toString();
}
}

View file

@ -92,21 +92,30 @@ import 'dart:collection' show HashMap, ListMixin;
import 'dart:indexed_db' show KeyRange;
import 'dart:typed_data' show TypedData;
import 'dart:_foreign_helper' show JS, DART_CLOSURE_TO_JS;
import 'dart:_interceptors' show JavaScriptObject, UnknownJavaScriptObject;
import 'dart:_js_helper' show Primitives, convertDartClosureToJS,
getIsolateAffinityTag;
import 'dart:_foreign_helper' show JS, JS_CONST, DART_CLOSURE_TO_JS;
import 'dart:_interceptors'
show JavaScriptObject, UnknownJavaScriptObject, DART_CLOSURE_PROPERTY_NAME;
import 'dart:_js_helper'
show Primitives, convertDartClosureToJS, getIsolateAffinityTag;
export 'dart:_interceptors' show JavaScriptObject;
final JsObject context = _wrapToDart(JS('', 'self'));
_convertDartFunction(Function f, {bool captureThis: false}) {
return JS('',
'function(_call, f, captureThis) {'
'return function() {'
'return _call(f, captureThis, this, '
'Array.prototype.slice.apply(arguments));'
'}'
'}(#, #, #)', DART_CLOSURE_TO_JS(_callDartFunction), f, captureThis);
return JS(
'',
'''
function(_call, f, captureThis) {
return function() {
return _call(f, captureThis, this,
Array.prototype.slice.apply(arguments));
}
}(#, #, #)
''',
DART_CLOSURE_TO_JS(_callDartFunction),
f,
captureThis);
}
_callDartFunction(callback, bool captureThis, self, List arguments) {
@ -212,8 +221,7 @@ class JsObject {
*/
factory JsObject.fromBrowserObject(object) {
if (object is num || object is String || object is bool || object == null) {
throw new ArgumentError(
"object cannot be a num, string, bool, or null");
throw new ArgumentError("object cannot be a num, string, bool, or null");
}
return _wrapToDart(_convertToJS(object));
}
@ -267,7 +275,7 @@ class JsObject {
*
* The type of [property] must be either [String] or [num].
*/
dynamic operator[](property) {
dynamic operator [](property) {
if (property is! String && property is! num) {
throw new ArgumentError("property is not a String or num");
}
@ -280,7 +288,7 @@ class JsObject {
*
* The type of [property] must be either [String] or [num].
*/
operator[]=(property, value) {
operator []=(property, value) {
if (property is! String && property is! num) {
throw new ArgumentError("property is not a String or num");
}
@ -289,8 +297,8 @@ class JsObject {
int get hashCode => 0;
bool operator==(other) => other is JsObject &&
JS('bool', '# === #', _jsObject, other._jsObject);
bool operator ==(other) =>
other is JsObject && JS('bool', '# === #', _jsObject, other._jsObject);
/**
* Returns `true` if the JavaScript object contains the specified property
@ -332,7 +340,7 @@ class JsObject {
String toString() {
try {
return JS('String', 'String(#)', _jsObject);
} catch(e) {
} catch (e) {
return super.toString();
}
}
@ -347,7 +355,11 @@ class JsObject {
if (method is! String && method is! num) {
throw new ArgumentError("method is not a String or num");
}
return _convertToDart(JS('', '#[#].apply(#, #)', _jsObject, method,
return _convertToDart(JS(
'',
'#[#].apply(#, #)',
_jsObject,
method,
_jsObject,
args == null ? null : new List.from(args.map(_convertToJS))));
}
@ -357,7 +369,6 @@ class JsObject {
* Proxies a JavaScript Function object.
*/
class JsFunction extends JsObject {
/**
* Returns a [JsFunction] that captures its 'this' binding and calls [f]
* with the value of this passed as the first argument.
@ -373,17 +384,18 @@ class JsFunction extends JsObject {
* Invokes the JavaScript function with arguments [args]. If [thisArg] is
* supplied it is the value of `this` for the invocation.
*/
dynamic apply(List args, { thisArg }) =>
_convertToDart(JS('', '#.apply(#, #)', _jsObject,
_convertToJS(thisArg),
args == null ? null : new List.from(args.map(_convertToJS))));
dynamic apply(List args, {thisArg}) => _convertToDart(JS(
'',
'#.apply(#, #)',
_jsObject,
_convertToJS(thisArg),
args == null ? null : new List.from(args.map(_convertToJS))));
}
/**
* A [List] that proxies a JavaScript array.
*/
class JsArray<E> extends JsObject with ListMixin<E> {
/**
* Creates a new JavaScript array.
*/
@ -449,8 +461,9 @@ class JsArray<E> extends JsObject with ListMixin<E> {
throw new StateError('Bad JsArray length');
}
set length(int length) { super['length'] = length; }
void set length(int length) {
super['length'] = length;
}
// Methods overriden for better performance
@ -503,12 +516,11 @@ class JsArray<E> extends JsObject with ListMixin<E> {
// property added to a Dart object referencing its JS-side DartObject proxy
final String _DART_OBJECT_PROPERTY_NAME =
getIsolateAffinityTag(r'_$dart_dartObject');
final String _DART_CLOSURE_PROPERTY_NAME =
getIsolateAffinityTag(r'_$dart_dartClosure');
// property added to a JS object referencing its Dart-side JsObject proxy
const _JS_OBJECT_PROPERTY_NAME = r'_$dart_jsObject';
const _JS_FUNCTION_PROPERTY_NAME = r'$dart_jsFunction';
const _JS_FUNCTION_PROPERTY_NAME_CAPTURE_THIS = r'_$dart_jsFunctionCaptureThis';
bool _defineProperty(o, String name, value) {
try {
@ -520,9 +532,9 @@ bool _defineProperty(o, String name, value) {
return true;
}
} catch (e) {
// object is native and lies about being extensible
// see https://bugzilla.mozilla.org/show_bug.cgi?id=775185
// Or, isExtensible throws for this object.
// object is native and lies about being extensible
// see https://bugzilla.mozilla.org/show_bug.cgi?id=775185
// Or, isExtensible throws for this object.
}
return false;
}
@ -555,8 +567,13 @@ dynamic _convertToJS(dynamic o) {
if (o is JsObject) {
return o._jsObject;
}
if (o is Blob || o is Event || o is KeyRange || o is ImageData || o is Node ||
o is TypedData || o is Window) {
if (o is Blob ||
o is Event ||
o is KeyRange ||
o is ImageData ||
o is Node ||
o is TypedData ||
o is Window) {
return o;
}
if (o is DateTime) {
@ -566,13 +583,13 @@ dynamic _convertToJS(dynamic o) {
return _getJsProxy(o, _JS_FUNCTION_PROPERTY_NAME, (o) {
var jsFunction = _convertDartFunction(o);
// set a property on the JS closure referencing the Dart closure
_defineProperty(jsFunction, _DART_CLOSURE_PROPERTY_NAME, o);
_defineProperty(jsFunction, DART_CLOSURE_PROPERTY_NAME, o);
return jsFunction;
});
}
var ctor = _dartProxyCtor;
return _getJsProxy(o, _JS_OBJECT_PROPERTY_NAME,
(o) => JS('', 'new #(#)', ctor, o));
return _getJsProxy(
o, _JS_OBJECT_PROPERTY_NAME, (o) => JS('', 'new #(#)', ctor, o));
}
Object _getJsProxy(o, String propertyName, createProxy(o)) {
@ -592,9 +609,14 @@ Object _convertToDart(o) {
JS('bool', 'typeof # == "number"', o) ||
JS('bool', 'typeof # == "boolean"', o)) {
return o;
} else if (_isLocalObject(o)
&& (o is Blob || o is Event || o is KeyRange || o is ImageData
|| o is Node || o is TypedData || o is Window)) {
} else if (_isLocalObject(o) &&
(o is Blob ||
o is Event ||
o is KeyRange ||
o is ImageData ||
o is Node ||
o is TypedData ||
o is Window)) {
// long line: dart2js doesn't allow string concatenation in the JS() form
return JS('Blob|Event|KeyRange|ImageData|Node|TypedData|Window', '#', o);
} else if (JS('bool', '# instanceof Date', o)) {
@ -609,15 +631,15 @@ Object _convertToDart(o) {
JsObject _wrapToDart(o) {
if (JS('bool', 'typeof # == "function"', o)) {
return _getDartProxy(o, _DART_CLOSURE_PROPERTY_NAME,
(o) => new JsFunction._fromJs(o));
return _getDartProxy(
o, DART_CLOSURE_PROPERTY_NAME, (o) => new JsFunction._fromJs(o));
}
if (JS('bool', '# instanceof Array', o)) {
return _getDartProxy(o, _DART_OBJECT_PROPERTY_NAME,
(o) => new JsArray._fromJs(o));
return _getDartProxy(
o, _DART_OBJECT_PROPERTY_NAME, (o) => new JsArray._fromJs(o));
}
return _getDartProxy(o, _DART_OBJECT_PROPERTY_NAME,
(o) => new JsObject._fromJs(o));
return _getDartProxy(
o, _DART_OBJECT_PROPERTY_NAME, (o) => new JsObject._fromJs(o));
}
Object _getDartProxy(o, String propertyName, createProxy(o)) {
@ -635,3 +657,72 @@ Object _getDartProxy(o, String propertyName, createProxy(o)) {
}
return dartProxy;
}
// ---------------------------------------------------------------------------
// Start of methods for new style Dart-JS interop.
_convertDartFunctionFast(Function f) {
var existing = JS('', '#.#', f, _JS_FUNCTION_PROPERTY_NAME);
if (existing != null) return existing;
var ret = JS(
'',
'''
function(_call, f) {
return function() {
return _call(f, Array.prototype.slice.apply(arguments));
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast),
f);
JS('', '#.# = #', ret, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', f, _JS_FUNCTION_PROPERTY_NAME, ret);
return ret;
}
_convertDartFunctionFastCaptureThis(Function f) {
var existing = JS('', '#.#', f, _JS_FUNCTION_PROPERTY_NAME_CAPTURE_THIS);
if (existing != null) return existing;
var ret = JS(
'',
'''
function(_call, f) {
return function() {
return _call(f, this,Array.prototype.slice.apply(arguments));
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFastCaptureThis),
f);
JS('', '#.# = #', ret, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', f, _JS_FUNCTION_PROPERTY_NAME_CAPTURE_THIS, ret);
return ret;
}
_callDartFunctionFast(callback, List arguments) {
return Function.apply(callback, arguments);
}
_callDartFunctionFastCaptureThis(callback, self, List arguments) {
return _convertToJS(Function.apply(callback, [self]..addAll(arguments)));
}
Function allowInterop(Function f) {
if (JS('bool', 'typeof(#) == "function"', f)) {
// Already supports interop, just use the existing function.
return f;
} else {
return _convertDartFunctionFast(f);
}
}
Function allowInteropCaptureThis(Function f) {
if (JS('bool', 'typeof(#) == "function"', f)) {
// Behavior when the function is already a JS function is unspecified.
throw new ArgumentError(
"Function is already a JS function so cannot capture this.");
return f;
} else {
return _convertDartFunctionFastCaptureThis(f);
}
}

View file

@ -381,8 +381,10 @@ const Map<String, String> DEFAULT_INTERCEPTORS_LIBRARY = const <String, String>{
'JSUInt31': 'class JSUInt31 extends JSUInt32 {}',
'JSUInt32': 'class JSUInt32 extends JSPositiveInt {}',
'ObjectInterceptor': 'class ObjectInterceptor {}',
'JavaScriptObject': 'class JavaScriptObject {}',
'PlainJavaScriptObject': 'class PlainJavaScriptObject {}',
'UnknownJavaScriptObject': 'class UnknownJavaScriptObject {}',
'JavaScriptFunction': 'class JavaScriptFunction {}',
};
const Map<String, String> DEFAULT_ISOLATE_HELPER_LIBRARY =

View file

@ -6,10 +6,12 @@ interactive_test: Skip # Must be run manually.
dromaeo_smoke_test: Skip # Issue 14521, 8257
cross_frame_test: Skip # Test reloads itself. Issue 18558
js_array_test: Skip # Issue 23676, 23677
js_typed_interop_test: Skip # Issue 23676, 23677
[ $compiler == none && ($runtime == dartium || $runtime == drt) ]
js_array_test: Skip # Dartium JSInterop failure
js_typed_interop_test: Skip # Dartium JSInterop failure
mirrors_js_typed_interop_test: Skip # Dartium JSInterop failure
cross_domain_iframe_test: RuntimeError # Dartium JSInterop failure
custom/document_register_type_extensions_test/registration: RuntimeError # Dartium JSInterop failure
custom/element_upgrade_test: RuntimeError # Dartium JSInterop failure
@ -385,10 +387,14 @@ request_animation_frame_test: Skip # Async test hangs.
[ $compiler == dart2js && $csp && ($runtime == drt || $runtime == safari || $runtime == ff || $runtime == chrome || $runtime == chromeOnAndroid) ]
# Note: these tests are all injecting scripts by design. This is not allowed under CSP.
event_customevent_test: Fail # Test cannot run under CSP restrictions.
js_interop_1_test: Skip # Test cannot run under CSP restrictions (times out).
js_test: Skip # Test cannot run under CSP restrictions (times out).
postmessage_structured_test: Skip # Test cannot run under CSP restrictions (times out).
event_customevent_test: Fail # Test cannot run under CSP restrictions.
js_interop_1_test: Skip # Test cannot run under CSP restrictions (times out).
js_test: Skip # Test cannot run under CSP restrictions (times out).
js_array_test: Skip # Test cannot run under CSP restrictions.
js_typed_interop_test: Skip # Test cannot run under CSP restrictions.
js_dart_to_string_test: Skip # Test cannot run under CSP restrictions.
mirrors_js_typed_interop_test: Skip # Test cannot run under CSP restrictions.
postmessage_structured_test: Skip # Test cannot run under CSP restrictions (times out).
[ $compiler == dart2js && $runtime == chrome]
svgelement_test/supported_altGlyph: RuntimeError # Issue 23144

View file

@ -2,165 +2,172 @@
// 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.
library jsArrayTest;
@Js("ArrayTest.Util")
library js_array_test;
import 'dart:html';
import 'dart:js';
import 'dart:js' as js;
import 'package:js/js.dart';
import 'package:unittest/unittest.dart';
import 'package:unittest/html_config.dart';
import 'json_helper.dart' as json_helper;
_injectJs() {
document.body.append(new ScriptElement()
..type = 'text/javascript'
..innerHtml = r"""
function callJsMethod(jsObj, jsMethodName, args) {
return jsObj[jsMethodName].apply(jsObj, args);
}
ArrayTest = {};
ArrayTest.Util = {
callJsMethod: function(jsObj, jsMethodName, args) {
return jsObj[jsMethodName].apply(jsObj, args);
},
function jsEnumerateIndices(obj) {
var ret = [];
for(var i in obj) {
ret.push(i);
}
return ret;
}
function setValue(obj, index, value) {
return obj[index] = value;
}
function getValue(obj, index) {
return obj[index];
}
function checkIsArray(obj) {
return Array.isArray(obj);
}
function concatValues(obj) {
return obj.concat("a", "b", ["c", "d"], 42, {foo: 10});
}
function concatOntoArray(obj) {
return [1,2,3].concat(obj, "foo");
}
function repeatedConcatOntoArray(obj) {
return [1,2,3].concat(obj, obj);
}
function everyGreaterThanZero(obj) {
return obj.every(function(currentValue, index, array) {
return currentValue > 0;
});
}
function everyGreaterThanZeroCheckThisArg(obj) {
var j = 0;
return obj.every(function(currentValue, index, array) {
if (j != index) {
throw "Unxpected index";
jsEnumerateIndices: function(obj) {
var ret = [];
for(var i in obj) {
ret.push(i);
}
j++;
if (array !== obj) {
throw "Array argument doesn't match obj";
}
return currentValue > 0;
});
}
return ret;
},
function filterGreater42(obj) {
return obj.filter(function(currentValue, index, array) {
return currentValue > 42;
});
}
checkIsArray: function(obj) {
return Array.isArray(obj);
},
function forEachCollectResult(array, callback) {
var result = [];
array.forEach(function(currentValue) {
result.push(currentValue * 2);
});
return result;
}
concatValues: function(obj) {
return obj.concat("a", "b", ["c", "d"], 42, {foo: 10});
},
function someEqual42(array, callback) {
return array.some(function(currentValue) {
return currentValue == 42;
});
}
concatOntoArray: function(obj) {
return [1,2,3].concat(obj, "foo");
},
function sortNumbersBackwards(array) {
return array.sort(function(a, b) {
return b - a;
});
}
repeatedConcatOntoArray: function(obj) {
return [1,2,3].concat(obj, obj);
},
function spliceDummyItems(array) {
return array.splice(1, 2, "quick" ,"brown", "fox");
}
everyGreaterThanZero: function(obj) {
return obj.every(function(currentValue, index, array) {
return currentValue > 0;
});
},
function spliceTestStringArgs(array) {
return array.splice("1.2", "2.01", "quick" ,"brown", "fox");
}
everyGreaterThanZeroCheckThisArg: function(obj) {
var j = 0;
return obj.every(function(currentValue, index, array) {
if (j != index) {
throw "Unxpected index";
}
j++;
if (array !== obj) {
throw "Array argument doesn't match obj";
}
return currentValue > 0;
});
},
function splicePastEnd(array) {
return array.splice(1, 5332, "quick" ,"brown", "fox");
}
filterGreater42: function(obj) {
return obj.filter(function(currentValue, index, array) {
return currentValue > 42;
});
},
function callJsToString(array) {
return array.toString();
}
forEachCollectResult: function(array) {
var result = [];
array.forEach(function(currentValue) {
result.push(currentValue * 2);
});
return result;
},
function mapAddIndexToEachElement(array) {
return array.map(function(currentValue, index) {
return currentValue + index;
});
}
someEqual42: function(array) {
return array.some(function(currentValue) {
return currentValue == 42;
});
},
function reduceSumDoubledElements(array) {
return array.reduce(function(previousValue, currentValue) {
return previousValue + currentValue*2;
},
0);
}
sortNumbersBackwards: function(array) {
return array.sort(function(a, b) {
return b - a;
});
},
// TODO(jacobr): add a test that distinguishes reduce from reduceRight.
function reduceRightSumDoubledElements(array) {
return array.reduceRight(function(previousValue, currentValue) {
return previousValue + currentValue*2;
},
0);
}
spliceDummyItems: function(array) {
return array.splice(1, 2, "quick" ,"brown", "fox");
},
function identical(o1, o2) {
return o1 === o2;
}
spliceTestStringArgs: function(array) {
return array.splice("1.2", "2.01", "quick" ,"brown", "fox");
},
function getOwnPropertyDescriptorJson(array, property) {
return JSON.stringify(Object.getOwnPropertyDescriptor(array, property));
}
splicePastEnd: function(array) {
return array.splice(1, 5332, "quick" ,"brown", "fox");
},
function setLength(array, len) {
return array.length = len;
}
callJsToString: function(array) {
return array.toString();
},
function jsonStringify(o) {
return JSON.stringify(o);
}
mapAddIndexToEachElement: function(array) {
return array.map(function(currentValue, index) {
return currentValue + index;
});
},
// Calling a method from Dart List on an arbitrary target object.
function callListMethodOnTarget(dartArray, target, methodName, args) {
return dartArray[methodName].apply(target, args);
}
reduceSumDoubledElements: function(array) {
return array.reduce(function(previousValue, currentValue) {
return previousValue + currentValue*2;
},
0);
},
// TODO(jacobr): add a test that distinguishes reduce from reduceRight.
reduceRightSumDoubledElements: function(array) {
return array.reduceRight(function(previousValue, currentValue) {
return previousValue + currentValue*2;
},
0);
},
getOwnPropertyDescriptorJson: function(array, property) {
return JSON.stringify(Object.getOwnPropertyDescriptor(array, property));
},
setLength: function(array, len) {
return array.length = len;
},
getValue: function(obj, index) {
return obj[index];
},
setValue: function(obj, index, value) {
return obj[index] = value;
},
// Calling a method from Dart List on an arbitrary target object.
callListMethodOnTarget: function(dartArray, target, methodName, args) {
return dartArray[methodName].apply(target, args);
},
newArray: function() { return []; },
newLiteral: function() { return {}; },
};
""");
}
@Js()
class SimpleJsLiteralClass {
external get foo;
}
class Foo {}
callJsMethod(List array, String methodName, List args) =>
context.callMethod("callJsMethod", [array, methodName, args]);
@Js()
external callJsMethod(List array, String methodName, List args);
callIndexOf(List array, value) => callJsMethod(array, "indexOf", [value]);
callLastIndexOf(List array, value) =>
@ -170,13 +177,78 @@ callPop(List array) => callJsMethod(array, "pop", []);
callPush(List array, element) => callJsMethod(array, "push", [element]);
callShift(List array) => callJsMethod(array, "shift", []);
callReverse(List array) => callJsMethod(array, "reverse", []);
callSetLength(List array, length) =>
context.callMethod("setLength", [array, length]);
callListMethodOnObject(JsObject object, String methodName, List args) => context
.callMethod("callListMethodOnTarget", [[], object, methodName, args]);
callListMethodOnObject(object, String methodName, List args) =>
callListMethodOnTarget([], object, methodName, args);
jsonStringify(JsObject object) => context.callMethod("jsonStringify", [object]);
@Js()
external jsEnumerateIndices(obj);
@Js()
external bool checkIsArray(obj);
@Js()
external concatValues(obj);
@Js()
external concatOntoArray(obj);
@Js()
external repeatedConcatOntoArray(obj);
@Js()
external bool everyGreaterThanZero(obj);
@Js()
external bool everyGreaterThanZeroCheckThisArg(obj);
@Js()
external filterGreater42(obj);
@Js()
external forEachCollectResult(List array);
@Js()
external someEqual42(List array);
@Js()
external sortNumbersBackwards(List array);
@Js()
external List spliceDummyItems(List array);
@Js()
external List spliceTestStringArgs(List array);
@Js()
external List splicePastEnd(List array);
@Js()
external String callJsToString(List array);
@Js()
external mapAddIndexToEachElement(List array);
@Js()
external reduceSumDoubledElements(List array);
// TODO(jacobr): add a test that distinguishes reduce from reduceRight.
@Js()
external reduceRightSumDoubledElements(List array);
@Js()
external getOwnPropertyDescriptorJson(List array, property);
@Js("setLength")
external callSetLength(List array, length);
@Js()
external getValue(obj, index);
@Js()
external setValue(obj, index, value);
@Js()
external callListMethodOnTarget(List target, object, String methodName, List args);
@Js()
external newArray();
@Js()
external newLiteral();
main() {
_injectJs();
@ -238,7 +310,7 @@ main() {
test('default', () {
expect(callJsMethod(list, "join", []), equals("3,42,foo"));
expect(callJsMethod(listWithDartClasses, "join", []),
equals("3,Instance of 'Foo',42,foo,Instance of 'Object'"));
equals("3,${new Foo()},42,foo,${new Object()}"));
});
test('custom separator', () {
@ -375,7 +447,8 @@ main() {
group("js snippet tests", () {
test("enumerate indices", () {
var list = ["a", "b", "c", "d"];
var indices = context.callMethod('jsEnumerateIndices', [list]);
var indices =
jsEnumerateIndices(list);
expect(indices.length, equals(4));
for (int i = 0; i < 4; i++) {
expect(indices[i], equals('$i'));
@ -384,51 +457,56 @@ main() {
test("set element", () {
var list = ["a", "b", "c", "d"];
context.callMethod('setValue', [list, 0, 42]);
setValue(list, 0, 42);
expect(list[0], equals(42));
context.callMethod('setValue', [list, 1, 84]);
setValue(list, 1, 84);
expect(list[1], equals(84));
context.callMethod(
'setValue', [list, 6, 100]); // Off the end of the list.
setValue(list, 6, 100); // Off the end of the list.
expect(list.length, equals(7));
expect(list[4], equals(null));
expect(list[6], equals(100));
// These tests have to be commented out because we don't persist
// JS proxies for Dart objects like we could/should.
// context.callMethod('setValue', [list, -1, "foo"]); // Not a valid array index
// expect(context.callMethod('getValue', [list, -1]), equals("foo"));
// expect(context.callMethod('getValue', [list, "-1"]), equals("foo"));
// setValue(list, -1, "foo"); // Not a valid array index
// expect(getValue(list, -1), equals("foo"));
// expect(getValue(list, "-1"), equals("foo"));
});
test("get element", () {
var list = ["a", "b", "c", "d"];
expect(context.callMethod('getValue', [list, 0]), equals("a"));
expect(context.callMethod('getValue', [list, 1]), equals("b"));
expect(context.callMethod('getValue', [list, 6]), equals(null));
expect(context.callMethod('getValue', [list, -1]), equals(null));
expect(getValue(list, 0),
equals("a"));
expect(getValue(list, 1),
equals("b"));
expect(getValue(list, 6),
equals(null));
expect(getValue(list, -1),
equals(null));
expect(context.callMethod('getValue', [list, "0"]), equals("a"));
expect(context.callMethod('getValue', [list, "1"]), equals("b"));
expect(getValue(list, "0"),
equals("a"));
expect(getValue(list, "1"),
equals("b"));
});
test("is array", () {
var list = ["a", "b"];
expect(context.callMethod("checkIsArray", [list]), isTrue);
expect(checkIsArray(list), isTrue);
});
test("property descriptors", () {
// This test matters to make behavior consistent with JS native arrays
// and to make devtools integration work well.
var list = ["a", "b"];
expect(context.callMethod("getOwnPropertyDescriptorJson", [list, 0]),
expect(getOwnPropertyDescriptorJson(list, 0),
equals('{"value":"a",'
'"writable":true,'
'"enumerable":true,'
'"configurable":true}'));
expect(
context.callMethod("getOwnPropertyDescriptorJson", [list, "length"]),
getOwnPropertyDescriptorJson(list, "length"),
equals('{"value":2,'
'"writable":true,'
'"enumerable":false,'
@ -440,21 +518,22 @@ main() {
// Tests that calling the concat method from JS will flatten out JS arrays
// We concat the array with "a", "b", ["c", "d"], 42, {foo: 10}
// which should generate ["1", "2", "a", "b", ["c", "d"], 42, {foo: 10}]
var ret = context.callMethod("concatValues", [list]);
var ret = concatValues(list);
expect(list.length, equals(2));
expect(ret.length, equals(8));
expect(ret[0], equals("1"));
expect(ret[3], equals("b"));
expect(ret[5], equals("d"));
expect(ret[6], equals(42));
expect(ret[7]['foo'], equals(10));
SimpleJsLiteralClass item = ret[7];
expect(item.foo, equals(10));
});
test("concat onto arrays", () {
// This test only passes if we have monkey patched the core Array object
// prototype to handle Dart Lists.
var list = ["a", "b"];
var ret = context.callMethod("concatOntoArray", [list]);
var ret = concatOntoArray(list);
expect(list.length, equals(2));
expect(ret, equals([1, 2, 3, "a", "b", "foo"]));
});
@ -463,47 +542,62 @@ main() {
// This test only passes if we have monkey patched the core Array object
// prototype to handle Dart Lists.
var list = ["a", "b"];
var ret = callJsMethod(list, "concat", [["c", "d"], "e", ["f", "g"]]);
var ret = callJsMethod(list, "concat", [
["c", "d"],
"e",
["f", "g"]
]);
expect(list.length, equals(2));
expect(ret, equals(["a", "b", "c", "d", "e", "f", "g"]));
});
test("every greater than zero", () {
expect(context.callMethod("everyGreaterThanZero", [[1, 5]]), isTrue);
expect(context.callMethod("everyGreaterThanZeroCheckThisArg", [[1, 5]]),
expect(
everyGreaterThanZero([1, 5]),
isTrue);
expect(
everyGreaterThanZeroCheckThisArg([1, 5]),
isTrue);
expect(
everyGreaterThanZero([1, 0]),
isFalse);
expect(everyGreaterThanZero([]),
isTrue);
expect(context.callMethod("everyGreaterThanZero", [[1, 0]]), isFalse);
expect(context.callMethod("everyGreaterThanZero", [[]]), isTrue);
});
test("filter greater than 42", () {
expect(context.callMethod("filterGreater42", [[1, 5]]), equals([]));
expect(context.callMethod("filterGreater42", [[43, 5, 49]]),
expect(filterGreater42([1, 5]), equals([]));
expect(
filterGreater42([43, 5, 49]),
equals([43, 49]));
expect(context.callMethod("filterGreater42", [["43", "5", "49"]]),
expect(
filterGreater42(["43", "5", "49"]),
equals(["43", "49"]));
});
test("for each collect result", () {
expect(context.callMethod("forEachCollectResult", [[1, 5, 7]]),
expect(
forEachCollectResult([1, 5, 7]),
equals([2, 10, 14]));
});
test("some", () {
expect(context.callMethod("someEqual42", [[1, 5, 9]]), isFalse);
expect(context.callMethod("someEqual42", [[1, 42, 9]]), isTrue);
expect(someEqual42([1, 5, 9]),
isFalse);
expect(someEqual42([1, 42, 9]),
isTrue);
});
test("sort backwards", () {
var arr = [1, 5, 9];
var ret = context.callMethod("sortNumbersBackwards", [arr]);
var ret = sortNumbersBackwards(arr);
expect(identical(arr, ret), isTrue);
expect(ret, equals([9, 5, 1]));
});
test("splice dummy items", () {
var list = [1, 2, 3, 4];
var removed = context.callMethod("spliceDummyItems", [list]);
var removed = spliceDummyItems(list);
expect(removed.length, equals(2));
expect(removed[0], equals(2));
expect(removed[1], equals(3));
@ -516,7 +610,7 @@ main() {
test("splice string args", () {
var list = [1, 2, 3, 4];
var removed = context.callMethod("spliceTestStringArgs", [list]);
var removed = spliceTestStringArgs(list);
expect(removed.length, equals(2));
expect(removed[0], equals(2));
expect(removed[1], equals(3));
@ -529,7 +623,7 @@ main() {
test("splice pastEndOfArray", () {
var list = [1, 2, 3, 4];
var removed = context.callMethod("splicePastEnd", [list]);
var removed = splicePastEnd(list);
expect(removed.length, equals(3));
expect(list.first, equals(1));
expect(list.length, equals(4));
@ -540,7 +634,7 @@ main() {
test("splice both bounds past end of array", () {
var list = [1];
var removed = context.callMethod("splicePastEnd", [list]);
var removed = splicePastEnd(list);
expect(removed.length, equals(0));
expect(list.first, equals(1));
expect(list.length, equals(4));
@ -550,25 +644,25 @@ main() {
});
test("call List method on JavaScript object", () {
var jsObject = new JsObject.jsify({});
var jsObject = newLiteral();
callListMethodOnObject(jsObject, 'push', ["a"]);
callListMethodOnObject(jsObject, 'push', ["b"]);
callListMethodOnObject(jsObject, 'push', ["c", "d"]);
callListMethodOnObject(jsObject, 'push', []);
expect(jsonStringify(jsObject),
expect(json_helper.stringify(jsObject),
equals('{"0":"a","1":"b","2":"c","3":"d","length":4}'));
expect(callListMethodOnObject(jsObject, 'pop', []), equals("d"));
expect(callListMethodOnObject(jsObject, 'join', ["#"]), equals("a#b#c"));
var jsArray = new JsObject.jsify([]);
var jsArray = newArray();
callListMethodOnObject(jsArray, 'push', ["a"]);
callListMethodOnObject(jsArray, 'push', ["b"]);
callListMethodOnObject(jsArray, 'push', ["c", "d"]);
callListMethodOnObject(jsArray, 'push', []);
expect(jsonStringify(jsArray), equals('["a","b","c","d"]'));
expect(json_helper.stringify(jsArray), equals('["a","b","c","d"]'));
});
});
@ -584,10 +678,10 @@ main() {
var listView = new UnmodifiableListView(list.getRange(1,3));
expect(listView is List, isTrue);
expect(listView.length, equals(2));
expect(context.callMethod("checkIsArray", [listView]), isFalse);
expect(context.callMethod("checkIsArray", [listView.toList()]), isTrue);
expect(context.callMethod("getOwnPropertyDescriptorJson",
[listView, "length"]), equals("null"));
expect(checkIsArray(listView), isFalse);
expect(checkIsArray(listView.toList()), isTrue);
expect(getOwnPropertyDescriptorJson(
listView, "length"), equals("null"));
});
});
*/

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015, 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.
@Js()
library js_typed_interop_test;
import 'dart:html';
import 'package:js/js.dart';
import 'package:unittest/unittest.dart';
import 'package:unittest/html_config.dart';
_injectJs() {
document.body.append(new ScriptElement()
..type = 'text/javascript'
..innerHtml = r"""
function jsToStringViaCoercion(a) {
return a + '';
};
""");
}
@Js()
external String jsToStringViaCoercion(obj);
class ExampleClassWithCustomToString {
var x;
ExampleClassWithCustomToString(this.x);
String toString() => "#$x#";
}
main() {
_injectJs();
useHtmlConfiguration();
group('toString', () {
test('custom dart', () {
var x = new ExampleClassWithCustomToString("fooBar");
expect(jsToStringViaCoercion(x), equals("#fooBar#"));
expect(jsToStringViaCoercion({'a' : 1, 'b': 2}), equals("{a: 1, b: 2}"));
});
});
}

View file

@ -2,24 +2,45 @@
// 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.
library jsArrayTest;
@Js()
library js_typed_interop_test;
import 'dart:html';
import 'dart:js';
import 'package:js/js.dart';
import 'package:unittest/unittest.dart';
import 'package:unittest/html_config.dart';
import 'package:unittest/html_individual_config.dart';
_injectJs() {
document.body.append(new ScriptElement()
..type = 'text/javascript'
..innerHtml = r"""
var Foo = {
multiplyDefault2: function(a, b) {
if (arguments.length >= 2) return a *b;
return a * 2;
}
};
var foo = {
x: 3,
z: 40, // Not specified in typed Dart API so should fail in checked mode.
multiplyByX: function(arg) { return arg * this.x; },
// This function can be torn off without having to bind this.
multiplyBy2: function(arg) { return arg * 2; }
multiplyBy2: function(arg) { return arg * 2; },
callClosureWithArg1: function(closure, arg) {
return closure(arg);
},
callClosureWithArg2: function(closure, arg1, arg2) {
return closure(arg1, arg2);
},
callClosureWithArgAndThis: function(closure, arg) {
return closure.apply(this, [arg]);
},
getBar: function() {
return bar;
}
};
var foob = {
@ -30,51 +51,145 @@ _injectJs() {
var bar = {
x: "foo",
multiplyByX: true
multiplyByX: true,
getFoo: function() {
return foo;
}
};
function ClassWithConstructor(a, b) {
this.a = a;
this.b = b;
};
ClassWithConstructor.prototype = {
getA: function() { return this.a;}
};
var selection = ["a", "b", "c", foo, bar];
selection.doubleLength = function() { return this.length * 2; };
function returnNumArgs() { return arguments.length; };
function returnLastArg() { return arguments[arguments.length-1]; };
function confuse(obj) { return obj; }
function StringWrapper(str) {
this.str = str;
}
StringWrapper.prototype = {
charCodeAt: function(index) {
return this.str.charCodeAt(index);
}
};
""");
}
abstract class Foo {
int get x;
set x(int v);
num multiplyByX(num y);
num multiplyBy2(num y);
class RegularClass {
factory RegularClass(a) {
return new RegularClass.fooConstructor(a);
}
RegularClass.fooConstructor(this.a);
var a;
}
abstract class Foob extends Foo {
final String y;
@Js()
class ClassWithConstructor {
external ClassWithConstructor(aParam, bParam);
external getA();
external get a;
external get b;
}
abstract class Bar {
String get x;
bool get multiplyByX;
@Js()
class Foo {
external int get x;
external set x(int v);
external num multiplyByX(num y);
external num multiplyBy2(num y);
external callClosureWithArgAndThis(Function closure, arg);
external callClosureWithArg1(Function closure, arg1);
external callClosureWithArg2(Function closure, arg1, arg2);
external Bar getBar();
external static int multiplyDefault2(int a, [int b]);
}
class Baz {}
@Js()
class ExampleLiteral {
external factory ExampleLiteral({int x, String y, num z});
// This class shows the pattern used by APIs such as jQuery that add methods
// to Arrays.
abstract class Selection implements List {
num doubleLength();
external int get x;
external String get y;
external num get z;
}
Foo get foo => context['foo'];
Foob get foob => context['foob'];
Bar get bar => context['bar'];
Selection get selection => context['selection'];
@Js('Foob')
class Foob extends Foo {
external String get y;
}
@Js('Bar')
class Bar
{
external String get x;
external bool get multiplyByX;
external Foo getFoo();
}
// No @Js is required for these external methods as the library is
// annotated with Js.
external Foo get foo;
external Foob get foob;
external Bar get bar;
external Selection get selection;
addWithDefault(a, [b = 100]) => a + b;
external Function get returnNumArgs;
external Function get returnLastArg;
const STRINGIFY_LOCATION = "JSON.stringify";
@Js(STRINGIFY_LOCATION)
external String stringify(obj);
@Js()
class StringWrapper {
external StringWrapper(String str);
external int charCodeAt(int i);
}
// Defeat JS type inference by calling through JavaScript interop.
@Js()
external confuse(obj);
main() {
// Call experimental API to register Dart interfaces implemented by
// JavaScript classes.
registerJsInterfaces([Foo, Foob, Bar, Selection]);
_injectJs();
useHtmlConfiguration();
useHtmlIndividualConfiguration();
group('object literal', () {
test('simple', () {
var l = new ExampleLiteral(x: 3, y: "foo");
expect(l.x, equals(3));
expect(l.y, equals("foo"));
expect(l.z, isNull);
expect(stringify(l), equals('{"x":3,"y":"foo"}'));
l = new ExampleLiteral(z: 100);
expect(l.x, isNull);
expect(l.y, isNull);
expect(l.z, equals(100));
expect(stringify(l), equals('{"z":100}'));
});
});
group('constructor', () {
test('simple', () {
var o = new ClassWithConstructor("foo", "bar");
expect(o.a, equals("foo"));
expect(o.b, equals("bar"));
expect(o.getA(), equals("foo"));
});
});
group('property', () {
test('get', () {
@ -83,15 +198,15 @@ main() {
expect(foob.y, equals("why"));
// Exists in JS but not in API.
expect(() => foo.z, throws);
expect(() => (foo as dynamic).zSomeInvalidName, throws);
expect(bar.multiplyByX, isTrue);
});
test('set', () {
foo.x = 42;
expect(foo.x, equals(42));
// Property tagged as read only in typed API.
expect(() => foob.y = "bla", throws);
expect(() => foo.unknownName = 42, throws);
expect(() => (foob as dynamic).y = "bla", throws);
expect(() => (foo as dynamic).unknownName = 42, throws);
});
});
@ -105,22 +220,96 @@ main() {
test('tearoff', () {
foo.x = 10;
// TODO(jacobr): should we automatically bind "this" for tearoffs of JS
// objects?
JsFunction multiplyBy2 = foo.multiplyBy2;
Function multiplyBy2 = foo.multiplyBy2;
expect(multiplyBy2(5), equals(10));
Function multiplyByX = foo.multiplyByX;
// Tearing off a JS closure doesn't bind this.
// You will need to use the new method tearoff syntax to bind this.
expect(multiplyByX(4), isNaN);
});
});
group('static method', () {
test('call from dart', () {
expect(Foo.multiplyDefault2(6, 7), equals(42));
expect(Foo.multiplyDefault2(6), equals(12));
Function tearOffMethod = Foo.multiplyDefault2;
expect(tearOffMethod(6, 6), equals(36));
expect(tearOffMethod(6), equals(12));
});
});
group('closure', () {
test('call from js', () {
localClosure(x) => x * 10;
var wrappedLocalClosure = allowInterop(localClosure);
expect(
identical(allowInterop(localClosure), wrappedLocalClosure), isTrue);
expect(foo.callClosureWithArg1(wrappedLocalClosure, 10), equals(100));
expect(foo.callClosureWithArg1(wrappedLocalClosure, "a"),
equals("aaaaaaaaaa"));
expect(foo.callClosureWithArg1(allowInterop(addWithDefault), 10),
equals(110));
expect(foo.callClosureWithArg2(allowInterop(addWithDefault), 10, 20),
equals(30));
addThisXAndArg(Foo that, int arg) {
return foo.x + arg;
}
var wrappedCaptureThisClosure = allowInteropCaptureThis(addThisXAndArg);
foo.x = 20;
expect(foo.callClosureWithArgAndThis(wrappedCaptureThisClosure, 10),
equals(30));
foo.x = 50;
expect(foo.callClosureWithArgAndThis(wrappedCaptureThisClosure, 10),
equals(60));
expect(
identical(allowInteropCaptureThis(addThisXAndArg),
wrappedCaptureThisClosure),
isTrue);
});
test('call from dart', () {
var returnNumArgsFn = returnNumArgs;
var returnLastArgFn = returnLastArg;
expect(returnNumArgsFn(), equals(0));
expect(returnNumArgsFn("a", "b", "c"), equals(3));
expect(returnNumArgsFn("a", "b", "c", null, null), equals(5));
expect(returnNumArgsFn(1,2,3,4,5,6, null), equals(7));
expect(returnNumArgsFn(1,2,3,4,5,6,7,8), equals(8));
expect(returnLastArgFn(1,2,"foo"), equals("foo"));
expect(returnLastArgFn(1,2,3,4,5,6,"foo"), equals("foo"));
});
});
group('chain calls', () {
test("method calls", () {
// In dart2js make sure we still use interceptors when making nested
// calls to objects.
var bar = foo.getBar().getFoo().getBar().getFoo().getBar();
expect(bar.x, equals("foo"));
});
});
group('avoid leaks on dart:core', () {
test('String', () {
var s = confuse('Hello');
var stringWrapper = confuse(new StringWrapper('Hello'));
// Make sure we don't allow calling JavaScript methods on String.
expect(() => s.charCodeAt(0), throws);
expect(stringWrapper.charCodeAt(0), equals(72));
});
});
group('type check', () {
test('js interfaces', () {
expect(foo is JsObject, isTrue);
// Cross-casts are allowed.
// Is checks return true for all JavaScript interfaces.
expect(foo is Bar, isTrue);
expect(selection is JsArray, isTrue);
expect(foo is Foob, isTrue);
expect(selection is List, isTrue);
// We do know at runtime whether something is a JsArray or not.
expect(foo is JsArray, isFalse);
expect(foo is List, isFalse);
});
test('dart interfaces', () {
@ -128,12 +317,4 @@ main() {
expect(selection is List, isTrue);
});
});
group("registration", () {
test('repeated fails', () {
// The experimental registerJsInterfaces API has already been called so
// it cannot be called a second time.
expect(() => registerJsInterfaces([Baz]), throws);
});
});
}

View file

@ -0,0 +1,10 @@
// Copyright (c) 2015, 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.
@Js("JSON")
library json_helper;
import 'package:js/js.dart';
external String stringify(object);

View file

@ -0,0 +1,48 @@
// Copyright (c) 2015, 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.
library tests.html.mirrors_js_typed_interop_test;
import 'dart:mirrors';
import 'dart:html';
import 'package:js/js.dart';
import 'package:unittest/unittest.dart';
import 'package:unittest/html_config.dart';
_injectJs() {
document.body.append(new ScriptElement()
..type = 'text/javascript'
..innerHtml = r"""
window.foo = {
x: 3,
z: 100,
multiplyBy2: function(arg) { return arg * 2; },
};
""");
}
@Js()
external Foo get foo;
@Js()
class Foo {
external int get x;
external int set x(v);
external num multiplyBy2(num y);
}
main() {
_injectJs();
useHtmlConfiguration();
test('dynamic dispatch', () {
var f = foo;
expect(f.x, 3);
// JsInterop methods are not accessible using reflection.
expect(() => reflect(f).setField(#x, 123), throws);
expect(f.x, 3);
});
}