diff --git a/pkg/compiler/lib/src/common/backend_api.dart b/pkg/compiler/lib/src/common/backend_api.dart index f72ce5e10c3..4638207bd74 100644 --- a/pkg/compiler/lib/src/common/backend_api.dart +++ b/pkg/compiler/lib/src/common/backend_api.dart @@ -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(); } diff --git a/pkg/compiler/lib/src/cps_ir/type_propagation.dart b/pkg/compiler/lib/src/cps_ir/type_propagation.dart index 71efeba4f84..b397a423677 100644 --- a/pkg/compiler/lib/src/cps_ir/type_propagation.dart +++ b/pkg/compiler/lib/src/cps_ir/type_propagation.dart @@ -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); diff --git a/pkg/compiler/lib/src/diagnostics/messages.dart b/pkg/compiler/lib/src/diagnostics/messages.dart index 347b25b54cd..edaf69c59f4 100644 --- a/pkg/compiler/lib/src/diagnostics/messages.dart +++ b/pkg/compiler/lib/src/diagnostics/messages.dart @@ -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}'."), diff --git a/pkg/compiler/lib/src/elements/elements.dart b/pkg/compiler/lib/src/elements/elements.dart index ea4863b76c9..60fcb20f266 100644 --- a/pkg/compiler/lib/src/elements/elements.dart +++ b/pkg/compiler/lib/src/elements/elements.dart @@ -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); } diff --git a/pkg/compiler/lib/src/elements/modelx.dart b/pkg/compiler/lib/src/elements/modelx.dart index 0109684f7ba..6dfd7511605 100644 --- a/pkg/compiler/lib/src/elements/modelx.dart +++ b/pkg/compiler/lib/src/elements/modelx.dart @@ -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 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 diff --git a/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart b/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart index c1d51cc2f79..dd506519cba 100644 --- a/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart +++ b/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart @@ -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) { diff --git a/pkg/compiler/lib/src/js_backend/backend.dart b/pkg/compiler/lib/src/js_backend/backend.dart index 9d2bcb86665..147b9958fbf 100644 --- a/pkg/compiler/lib/src/js_backend/backend.dart +++ b/pkg/compiler/lib/src/js_backend/backend.dart @@ -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() { diff --git a/pkg/compiler/lib/src/js_backend/custom_elements_analysis.dart b/pkg/compiler/lib/src/js_backend/custom_elements_analysis.dart index e345a7c3b08..8ddbb43069d 100644 --- a/pkg/compiler/lib/src/js_backend/custom_elements_analysis.dart +++ b/pkg/compiler/lib/src/js_backend/custom_elements_analysis.dart @@ -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); } diff --git a/pkg/compiler/lib/src/js_backend/js_backend.dart b/pkg/compiler/lib/src/js_backend/js_backend.dart index 69f31967dc9..0af185f32df 100644 --- a/pkg/compiler/lib/src/js_backend/js_backend.dart +++ b/pkg/compiler/lib/src/js_backend/js_backend.dart @@ -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'; diff --git a/pkg/compiler/lib/src/js_backend/js_interop_analysis.dart b/pkg/compiler/lib/src/js_backend/js_interop_analysis.dart new file mode 100644 index 00000000000..777a36829bd --- /dev/null +++ b/pkg/compiler/lib/src/js_backend/js_interop_analysis.dart @@ -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 statements = []; + backend.compiler.codegenWorld.forEachInvokedName( + (String name, Map 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.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); + } +} diff --git a/pkg/compiler/lib/src/js_backend/namer.dart b/pkg/compiler/lib/src/js_backend/namer.dart index 5bb88de8ea9..783b5181750 100644 --- a/pkg/compiler/lib/src/js_backend/namer.dart +++ b/pkg/compiler/lib/src/js_backend/namer.dart @@ -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); } diff --git a/pkg/compiler/lib/src/js_backend/patch_resolver.dart b/pkg/compiler/lib/src/js_backend/patch_resolver.dart index 1b5dcbae2b8..c8e609de57a 100644 --- a/pkg/compiler/lib/src/js_backend/patch_resolver.dart +++ b/pkg/compiler/lib/src/js_backend/patch_resolver.dart @@ -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); } diff --git a/pkg/compiler/lib/src/js_emitter/full_emitter/setup_program_builder.dart b/pkg/compiler/lib/src/js_emitter/full_emitter/setup_program_builder.dart index 309e12108dc..e256e7b1bfa 100644 --- a/pkg/compiler/lib/src/js_emitter/full_emitter/setup_program_builder.dart +++ b/pkg/compiler/lib/src/js_emitter/full_emitter/setup_program_builder.dart @@ -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; } diff --git a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart index 1da1a032e8a..886d3ede86a 100644 --- a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart +++ b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart @@ -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)])); diff --git a/pkg/compiler/lib/src/js_emitter/native_emitter.dart b/pkg/compiler/lib/src/js_emitter/native_emitter.dart index d78ac6d1d7d..9f873350492 100644 --- a/pkg/compiler/lib/src/js_emitter/native_emitter.dart +++ b/pkg/compiler/lib/src/js_emitter/native_emitter.dart @@ -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 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])); diff --git a/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart b/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart index b9619809ad8..01d3f3846b5 100644 --- a/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart +++ b/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart @@ -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 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(); + librariesMap.forEach((LibraryElement library, List 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.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 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 methods = []; List callStubs = []; @@ -467,28 +571,35 @@ class ProgramBuilder { storeFunctionTypeInMetadata: _storeFunctionTypesInMetadata); List checkedSetters = []; - 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 isChecks = []; - 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; diff --git a/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart b/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart index 4fc5ab6dd7a..8b20e2de9d3 100644 --- a/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart +++ b/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart @@ -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'); } diff --git a/pkg/compiler/lib/src/native/enqueue.dart b/pkg/compiler/lib/src/native/enqueue.dart index 6b39359aa88..42ceba2d4b2 100644 --- a/pkg/compiler/lib/src/native/enqueue.dart +++ b/pkg/compiler/lib/src/native/enqueue.dart @@ -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)) { diff --git a/pkg/compiler/lib/src/patch_parser.dart b/pkg/compiler/lib/src/patch_parser.dart index 8d75b67cecd..a0ad4d9ac86 100644 --- a/pkg/compiler/lib/src/patch_parser.dart +++ b/pkg/compiler/lib/src/patch_parser.dart @@ -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 { } } +/// Annotation handler for pre-resolution detection of `@Js(...)` +/// annotations. +class JsInteropAnnotationHandler implements EagerAnnotationHandler { + 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 { const PatchAnnotationHandler(); diff --git a/pkg/compiler/lib/src/serialization/modelz.dart b/pkg/compiler/lib/src/serialization/modelz.dart index 2b931729b2f..86d27901124 100644 --- a/pkg/compiler/lib/src/serialization/modelz.dart +++ b/pkg/compiler/lib/src/serialization/modelz.dart @@ -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; diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart index e24af1149cb..bb00a2640ca 100644 --- a/pkg/compiler/lib/src/ssa/builder.dart +++ b/pkg/compiler/lib/src/ssa/builder.dart @@ -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 link, List list) { @@ -5101,7 +5128,8 @@ class SsaBuilder extends ast.Visitor var inputs = []; 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 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 = []; + var parameterNameMap = new Map(); + 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, + []); + 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 = [target]..addAll(arguments); + + js.Template codeTemplate; + if (element.isGetter) { + codeTemplate = js.js.parseForeignJS("#"); + } else if (element.isSetter) { + codeTemplate = js.js.parseForeignJS("# = #"); + } else { + var argsStub = []; + 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 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.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.from( + currentInlinedInstantiations); + } + instruction.sideEffects = compiler.world.getSideEffectsOfElement(element); } - instruction.sideEffects = compiler.world.getSideEffectsOfElement(element); if (location == null) { push(instruction); } else { diff --git a/pkg/compiler/lib/src/types/types.dart b/pkg/compiler/lib/src/types/types.dart index 42dbae5b766..9d91f99ff73 100644 --- a/pkg/compiler/lib/src/types/types.dart +++ b/pkg/compiler/lib/src/types/types.dart @@ -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; diff --git a/pkg/compiler/pubspec.yaml b/pkg/compiler/pubspec.yaml index f396ed039b2..c885986350e 100644 --- a/pkg/compiler/pubspec.yaml +++ b/pkg/compiler/pubspec.yaml @@ -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: diff --git a/pkg/js/AUTHORS b/pkg/js/AUTHORS new file mode 100644 index 00000000000..d773d3afcd9 --- /dev/null +++ b/pkg/js/AUTHORS @@ -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 + +Google Inc. + +Alexandre Ardhuin diff --git a/pkg/js/LICENSE b/pkg/js/LICENSE new file mode 100644 index 00000000000..abbb072f8be --- /dev/null +++ b/pkg/js/LICENSE @@ -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. diff --git a/pkg/js/PATENTS b/pkg/js/PATENTS new file mode 100644 index 00000000000..69541968b91 --- /dev/null +++ b/pkg/js/PATENTS @@ -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. diff --git a/pkg/js/README.md b/pkg/js/README.md new file mode 100644 index 00000000000..d27b6eb94ec --- /dev/null +++ b/pkg/js/README.md @@ -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 + + + + + + + +``` + +##### 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 diff --git a/pkg/js/lib/js.dart b/pkg/js/lib/js.dart new file mode 100644 index 00000000000..c31794b2a5b --- /dev/null +++ b/pkg/js/lib/js.dart @@ -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]); +} diff --git a/pkg/js/pubspec.yaml b/pkg/js/pubspec.yaml new file mode 100644 index 00000000000..16ed37c8c11 --- /dev/null +++ b/pkg/js/pubspec.yaml @@ -0,0 +1,9 @@ +name: js +version: 0.6.0-dev.1 +authors: +- Dart Team +- Alexandre Ardhuin +description: Access JavaScript from Dart. +homepage: https://github.com/dart-lang/js-interop +environment: + sdk: '>=1.13.0 <2.0.0' diff --git a/sdk/lib/_internal/js_runtime/lib/interceptors.dart b/sdk/lib/_internal/js_runtime/lib/interceptors.dart index b13d79a3fa5..25fbb117517 100644 --- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart +++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart @@ -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 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(); + } } diff --git a/sdk/lib/js/dart2js/js_dart2js.dart b/sdk/lib/js/dart2js/js_dart2js.dart index 8c5eb301d32..417876e5738 100644 --- a/sdk/lib/js/dart2js/js_dart2js.dart +++ b/sdk/lib/js/dart2js/js_dart2js.dart @@ -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 extends JsObject with ListMixin { - /** * Creates a new JavaScript array. */ @@ -449,8 +461,9 @@ class JsArray extends JsObject with ListMixin { 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 extends JsObject with ListMixin { // 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); + } +} diff --git a/tests/compiler/dart2js/mock_libraries.dart b/tests/compiler/dart2js/mock_libraries.dart index 95bb3fbe5d2..37135b2274d 100644 --- a/tests/compiler/dart2js/mock_libraries.dart +++ b/tests/compiler/dart2js/mock_libraries.dart @@ -381,8 +381,10 @@ const Map DEFAULT_INTERCEPTORS_LIBRARY = const { '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 DEFAULT_ISOLATE_HELPER_LIBRARY = diff --git a/tests/html/html.status b/tests/html/html.status index a683c76ef71..6670bd29010 100644 --- a/tests/html/html.status +++ b/tests/html/html.status @@ -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 diff --git a/tests/html/js_array_test.dart b/tests/html/js_array_test.dart index 4ede8ed70fb..039535eac1a 100644 --- a/tests/html/js_array_test.dart +++ b/tests/html/js_array_test.dart @@ -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")); }); }); */ diff --git a/tests/html/js_dart_to_string_test.dart b/tests/html/js_dart_to_string_test.dart new file mode 100644 index 00000000000..f702d6b978d --- /dev/null +++ b/tests/html/js_dart_to_string_test.dart @@ -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}")); + }); + }); +} diff --git a/tests/html/js_typed_interop_test.dart b/tests/html/js_typed_interop_test.dart index c8c02600743..fb69069b9fc 100644 --- a/tests/html/js_typed_interop_test.dart +++ b/tests/html/js_typed_interop_test.dart @@ -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); - }); - }); } diff --git a/tests/html/json_helper.dart b/tests/html/json_helper.dart new file mode 100644 index 00000000000..15f92c0e614 --- /dev/null +++ b/tests/html/json_helper.dart @@ -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); diff --git a/tests/html/mirrors_js_typed_interop_test.dart b/tests/html/mirrors_js_typed_interop_test.dart new file mode 100644 index 00000000000..20bb91df0ec --- /dev/null +++ b/tests/html/mirrors_js_typed_interop_test.dart @@ -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); + }); +}