From 96ca5db7e5ad4a7c259376b7ce1bf2d694ccf479 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Mon, 25 Jul 2016 09:59:01 -0700 Subject: [PATCH] Add JSNative utility class with static methods methods to efficiently manipulate typed JSInterop objects in cases where the member name is not known statically. These methods would be extension methods on JSObject if Dart supported extension methods. Update package js to export these methods. Implement in Dart2JS. Implement JS$ in dart2js. BUG= R=alanknight@google.com, sra@google.com Review URL: https://codereview.chromium.org/2150313003 . --- .../lib/src/js_backend/native_data.dart | 14 +- .../program_builder/program_builder.dart | 12 +- pkg/compiler/lib/src/native/native.dart | 1 + pkg/compiler/lib/src/ssa/builder.dart | 9 +- pkg/js/CHANGELOG.md | 6 + pkg/js/lib/js_util.dart | 8 + pkg/js/pubspec.yaml | 2 +- runtime/bin/bin.gypi | 35 ++ runtime/bin/builtin.cc | 1 + runtime/bin/builtin.h | 1 + .../sdk_library_metadata/lib/libraries.dart | 6 + sdk/lib/dart2dart.platform | 1 + sdk/lib/dart_client.platform | 1 + sdk/lib/dart_shared.platform | 1 + sdk/lib/js/dart2js/js_dart2js.dart | 2 +- sdk/lib/js/dartium/js_dartium.dart | 471 ++++++++++-------- sdk/lib/js_util/dart2js/js_util_dart2js.dart | 126 +++++ sdk/lib/js_util/dartium/js_util_dartium.dart | 38 ++ .../call_on_native_class_test.dart | 17 - .../dart2js_native/dart2js_native.status | 2 - tests/html/js_typed_interop_test.dart | 158 +++++- tests/html/js_util_test.dart | 347 +++++++++++++ tools/create_sdk.py | 3 +- 23 files changed, 1034 insertions(+), 228 deletions(-) create mode 100644 pkg/js/lib/js_util.dart create mode 100644 sdk/lib/js_util/dart2js/js_util_dart2js.dart create mode 100644 sdk/lib/js_util/dartium/js_util_dartium.dart delete mode 100644 tests/compiler/dart2js_native/call_on_native_class_test.dart create mode 100644 tests/html/js_util_test.dart diff --git a/pkg/compiler/lib/src/js_backend/native_data.dart b/pkg/compiler/lib/src/js_backend/native_data.dart index 67b473b53c5..31fe7d69fea 100644 --- a/pkg/compiler/lib/src/js_backend/native_data.dart +++ b/pkg/compiler/lib/src/js_backend/native_data.dart @@ -35,6 +35,10 @@ class NativeData { Map nativeFieldStoreBehavior = {}; + /// Prefix used to escape JS names that are not valid Dart names + /// when using JSInterop. + static const String _jsInteropEscapePrefix = r'JS$'; + /// Returns `true` if [element] is explicitly marked as part of JsInterop. bool _isJsInterop(Element element) { return jsInteropNames.containsKey(element.declaration); @@ -93,7 +97,7 @@ class NativeData { if (jsInteropName != null && jsInteropName.isNotEmpty) { return jsInteropName; } - return element.isLibrary ? 'self' : element.name; + return element.isLibrary ? 'self' : getUnescapedJSInteropName(element.name); } /// Computes the name for [element] to use in the generated JavaScript. This @@ -216,4 +220,12 @@ class NativeData { FieldElement field, NativeBehavior behavior) { nativeFieldStoreBehavior[field] = behavior; } + + /// Apply JS$ escaping scheme to convert possible escaped Dart names into + /// JS names. + String getUnescapedJSInteropName(String name) { + return name.startsWith(_jsInteropEscapePrefix) + ? name.substring(_jsInteropEscapePrefix.length) + : name; + } } 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 1f81f6d6b20..efec052e37b 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 @@ -345,6 +345,8 @@ class ProgramBuilder { for (Element e in elements) { if (e is ClassElement && backend.isJsInterop(e)) { e.declaration.forEachMember((_, Element member) { + var jsName = + backend.nativeData.getUnescapedJSInteropName(member.name); if (!member.isInstanceMember) return; if (member.isGetter || member.isField || member.isFunction) { var selectors = @@ -354,7 +356,7 @@ class ProgramBuilder { var stubName = namer.invocationName(selector); if (stubNames.add(stubName.key)) { interceptorClass.callStubs.add(_buildStubMethod(stubName, - js.js('function(obj) { return obj.# }', [member.name]), + js.js('function(obj) { return obj.# }', [jsName]), element: member)); } } @@ -367,10 +369,8 @@ class ProgramBuilder { 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]), + interceptorClass.callStubs.add(_buildStubMethod(stubName, + js.js('function(obj, v) { return obj.# = v }', [jsName]), element: member)); } } @@ -447,7 +447,7 @@ class ProgramBuilder { interceptorClass.callStubs.add(_buildStubMethod( stubName, js.js('function(receiver, #) { return receiver.#(#) }', - [parameters, member.name, parameters]), + [parameters, jsName, parameters]), element: member)); } } diff --git a/pkg/compiler/lib/src/native/native.dart b/pkg/compiler/lib/src/native/native.dart index c9c60f6996e..c0d6a869e81 100644 --- a/pkg/compiler/lib/src/native/native.dart +++ b/pkg/compiler/lib/src/native/native.dart @@ -19,6 +19,7 @@ const Iterable _allowedDartSchemePaths = const [ 'html_common', 'indexed_db', 'js', + 'js_util', 'svg', '_native_typed_data', 'web_audio', diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart index 9c7a31f397c..71485817e97 100644 --- a/pkg/compiler/lib/src/ssa/builder.dart +++ b/pkg/compiler/lib/src/ssa/builder.dart @@ -5619,14 +5619,15 @@ class SsaBuilder extends ast.Visitor 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. + // TODO(jacobr): consider throwing if parameter names do not match + // names of properties in the class. assert(parameter.isNamed); HInstruction argument = arguments[i]; if (argument != null) { filteredArguments.add(argument); - parameterNameMap[parameter.name] = - new js.InterpolatedExpression(positions++); + var jsName = + backend.nativeData.getUnescapedJSInteropName(parameter.name); + parameterNameMap[jsName] = new js.InterpolatedExpression(positions++); } i++; }); diff --git a/pkg/js/CHANGELOG.md b/pkg/js/CHANGELOG.md index 4ef2ebc5264..7438b4cf077 100644 --- a/pkg/js/CHANGELOG.md +++ b/pkg/js/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.6.1 +* Add js_util library of utility methods to efficiently manipulate typed + JavaScript interop objects in cases where the member name is not known + statically. These methods would be extension methods on JSObject if + Dart supported extension methods. + ## 0.6.0 * Version 0.6.0 is a complete rewrite of `package:js`. diff --git a/pkg/js/lib/js_util.dart b/pkg/js/lib/js_util.dart new file mode 100644 index 00000000000..1e47e5d4ec7 --- /dev/null +++ b/pkg/js/lib/js_util.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2016, 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. + +/// Allows interoperability with Javascript APIs. +library js_util; + +export 'dart:js_util'; diff --git a/pkg/js/pubspec.yaml b/pkg/js/pubspec.yaml index 75558762eca..1531b082a4c 100644 --- a/pkg/js/pubspec.yaml +++ b/pkg/js/pubspec.yaml @@ -1,5 +1,5 @@ name: js -version: 0.6.0 +version: 0.6.1 authors: - Dart Team description: Access JavaScript from Dart. diff --git a/runtime/bin/bin.gypi b/runtime/bin/bin.gypi index 03d67ee69df..43b5915ce1f 100644 --- a/runtime/bin/bin.gypi +++ b/runtime/bin/bin.gypi @@ -11,6 +11,7 @@ 'html_cc_file': '<(gen_source_dir)/html_gen.cc', 'html_common_cc_file': '<(gen_source_dir)/html_common_gen.cc', 'js_cc_file': '<(gen_source_dir)/js_gen.cc', + 'js_util_cc_file': '<(gen_source_dir)/js_util_gen.cc', 'blink_cc_file': '<(gen_source_dir)/blink_gen.cc', 'indexeddb_cc_file': '<(gen_source_dir)/indexeddb_gen.cc', 'cached_patches_cc_file': '<(gen_source_dir)/cached_patches_gen.cc', @@ -233,6 +234,38 @@ }, ] }, + { + 'target_name': 'generate_js_util_cc_file', + 'type': 'none', + 'toolsets':['host'], + 'sources': [ + '../../sdk/lib/js_util/dartium/js_util_dartium.dart', + ], + 'actions': [ + { + 'action_name': 'generate_js_util_cc', + 'inputs': [ + '../tools/gen_library_src_paths.py', + '<(builtin_in_cc_file)', + '<@(_sources)', + ], + 'outputs': [ + '<(js_util_cc_file)', + ], + 'action': [ + 'python', + 'tools/gen_library_src_paths.py', + '--output', '<(js_util_cc_file)', + '--input_cc', '<(builtin_in_cc_file)', + '--include', 'bin/builtin.h', + '--var_name', 'dart::bin::Builtin::js_util_source_paths_', + '--library_name', 'dart:js_util', + '<@(_sources)', + ], + 'message': 'Generating ''<(js_util_cc_file)'' file.' + }, + ] + }, { 'target_name': 'generate_blink_cc_file', 'type': 'none', @@ -500,6 +533,7 @@ 'generate_html_cc_file#host', 'generate_html_common_cc_file#host', 'generate_js_cc_file#host', + 'generate_js_util_cc_file#host', 'generate_blink_cc_file#host', 'generate_indexeddb_cc_file#host', 'generate_cached_patches_cc_file#host', @@ -1262,6 +1296,7 @@ '<(html_cc_file)', '<(html_common_cc_file)', '<(js_cc_file)', + '<(js_util_cc_file)', '<(blink_cc_file)', '<(indexeddb_cc_file)', '<(cached_patches_cc_file)', diff --git a/runtime/bin/builtin.cc b/runtime/bin/builtin.cc index c26a76293ac..04fb0600e87 100644 --- a/runtime/bin/builtin.cc +++ b/runtime/bin/builtin.cc @@ -24,6 +24,7 @@ Builtin::builtin_lib_props Builtin::builtin_libraries_[] = { { "dart:html", html_source_paths_, NULL, NULL, true }, { "dart:html_common", html_common_source_paths_, NULL, NULL, true}, { "dart:js", js_source_paths_, NULL, NULL, true}, + { "dart:js_util", js_util_source_paths_, NULL, NULL, true}, { "dart:_blink", blink_source_paths_, NULL, NULL, true }, { "dart:indexed_db", indexeddb_source_paths_, NULL, NULL, true }, { "cached_patches.dart", cached_patches_source_paths_, NULL, NULL, true }, diff --git a/runtime/bin/builtin.h b/runtime/bin/builtin.h index 5029cf1b30c..d90223af734 100644 --- a/runtime/bin/builtin.h +++ b/runtime/bin/builtin.h @@ -74,6 +74,7 @@ class Builtin { static const char* html_source_paths_[]; static const char* html_common_source_paths_[]; static const char* js_source_paths_[]; + static const char* js_util_source_paths_[]; static const char* blink_source_paths_[]; static const char* indexeddb_source_paths_[]; static const char* cached_patches_source_paths_[]; diff --git a/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart b/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart index e304873189f..0ecb558059f 100644 --- a/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart +++ b/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart @@ -116,6 +116,12 @@ const Map libraries = const { maturity: Maturity.STABLE, dart2jsPath: "js/dart2js/js_dart2js.dart"), + "js_util": const LibraryInfo( + "js_util/dartium/js_util_dartium.dart", + categories: "Client", + maturity: Maturity.STABLE, + dart2jsPath: "js_util/dart2js/js_util_dart2js.dart"), + "math": const LibraryInfo( "math/math.dart", categories: "Client,Server,Embedded", diff --git a/sdk/lib/dart2dart.platform b/sdk/lib/dart2dart.platform index fd9d8ed0d9d..d9aaa193dbe 100644 --- a/sdk/lib/dart2dart.platform +++ b/sdk/lib/dart2dart.platform @@ -25,6 +25,7 @@ indexed_db: indexed_db/dart2js/indexed_db_dart2js.dart io: io/io.dart isolate: isolate/isolate.dart js: js/dart2js/js_dart2js.dart +js_util: js_util/dart2js/js_util_dart2js.dart math: math/math.dart mirrors: mirrors/mirrors.dart nativewrappers: html/dart2js/nativewrappers.dart diff --git a/sdk/lib/dart_client.platform b/sdk/lib/dart_client.platform index 2e01cd991bc..38442ef971e 100644 --- a/sdk/lib/dart_client.platform +++ b/sdk/lib/dart_client.platform @@ -26,6 +26,7 @@ indexed_db: indexed_db/dart2js/indexed_db_dart2js.dart io: unsupported: isolate: isolate/isolate.dart js: js/dart2js/js_dart2js.dart +js_util: js_util/dart2js/js_util_dart2js.dart math: math/math.dart mirrors: mirrors/mirrors.dart nativewrappers: html/dart2js/nativewrappers.dart diff --git a/sdk/lib/dart_shared.platform b/sdk/lib/dart_shared.platform index 539b8688966..ae47359fcda 100644 --- a/sdk/lib/dart_shared.platform +++ b/sdk/lib/dart_shared.platform @@ -24,6 +24,7 @@ indexed_db: indexed_db/dart2js/indexed_db_dart2js.dart io: io/io.dart isolate: isolate/isolate.dart js: js/dart2js/js_dart2js.dart +js_util: js_util/dart2js/js_util_dart2js.dart math: math/math.dart mirrors: mirrors/mirrors.dart nativewrappers: html/dart2js/nativewrappers.dart diff --git a/sdk/lib/js/dart2js/js_dart2js.dart b/sdk/lib/js/dart2js/js_dart2js.dart index 4b3e4a63d09..61bf1da6251 100644 --- a/sdk/lib/js/dart2js/js_dart2js.dart +++ b/sdk/lib/js/dart2js/js_dart2js.dart @@ -707,7 +707,7 @@ _callDartFunctionFastCaptureThis(callback, self, List arguments) { return Function.apply(callback, [self]..addAll(arguments)); } -Function /*=F*/ allowInterop/**/(Function /*=F*/ f) { +Function/*=F*/ allowInterop/**/(Function/*=F*/ f) { if (JS('bool', 'typeof(#) == "function"', f)) { // Already supports interop, just use the existing function. return f; diff --git a/sdk/lib/js/dartium/js_dartium.dart b/sdk/lib/js/dartium/js_dartium.dart index f184cfb04a7..4ac11975db0 100644 --- a/sdk/lib/js/dartium/js_dartium.dart +++ b/sdk/lib/js/dartium/js_dartium.dart @@ -138,6 +138,114 @@ final _jsInterfaceTypes = new Set(); @Deprecated("Internal Use Only") Iterable get jsInterfaceTypes => _jsInterfaceTypes; +class _StringLiteralEscape { + // Character code constants. + static const int BACKSPACE = 0x08; + static const int TAB = 0x09; + static const int NEWLINE = 0x0a; + static const int CARRIAGE_RETURN = 0x0d; + static const int FORM_FEED = 0x0c; + static const int QUOTE = 0x22; + static const int CHAR_$ = 0x24; + static const int CHAR_0 = 0x30; + static const int BACKSLASH = 0x5c; + static const int CHAR_b = 0x62; + static const int CHAR_f = 0x66; + static const int CHAR_n = 0x6e; + static const int CHAR_r = 0x72; + static const int CHAR_t = 0x74; + static const int CHAR_u = 0x75; + + final StringSink _sink; + + _StringLiteralEscape(this._sink); + + void writeString(String string) { + _sink.write(string); + } + + void writeStringSlice(String string, int start, int end) { + _sink.write(string.substring(start, end)); + } + + void writeCharCode(int charCode) { + _sink.writeCharCode(charCode); + } + + /// ('0' + x) or ('a' + x - 10) + static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; + + /// Write, and suitably escape, a string's content as a JSON string literal. + void writeStringContent(String s) { + // Identical to JSON string literal escaping except that we also escape $. + int offset = 0; + final int length = s.length; + for (int i = 0; i < length; i++) { + int charCode = s.codeUnitAt(i); + if (charCode > BACKSLASH) continue; + if (charCode < 32) { + if (i > offset) writeStringSlice(s, offset, i); + offset = i + 1; + writeCharCode(BACKSLASH); + switch (charCode) { + case BACKSPACE: + writeCharCode(CHAR_b); + break; + case TAB: + writeCharCode(CHAR_t); + break; + case NEWLINE: + writeCharCode(CHAR_n); + break; + case FORM_FEED: + writeCharCode(CHAR_f); + break; + case CARRIAGE_RETURN: + writeCharCode(CHAR_r); + break; + default: + writeCharCode(CHAR_u); + writeCharCode(CHAR_0); + writeCharCode(CHAR_0); + writeCharCode(hexDigit((charCode >> 4) & 0xf)); + writeCharCode(hexDigit(charCode & 0xf)); + break; + } + } else if (charCode == QUOTE || + charCode == BACKSLASH || + charCode == CHAR_$) { + if (i > offset) writeStringSlice(s, offset, i); + offset = i + 1; + writeCharCode(BACKSLASH); + writeCharCode(charCode); + } + } + if (offset == 0) { + writeString(s); + } else if (offset < length) { + writeStringSlice(s, offset, length); + } + } + + /** + * Serialize a [num], [String], [bool], [Null], [List] or [Map] value. + * + * Returns true if the value is one of these types, and false if not. + * If a value is both a [List] and a [Map], it's serialized as a [List]. + */ + bool writeStringLiteral(String str) { + writeString('"'); + writeStringContent(str); + writeString('"'); + } +} + +String _escapeString(String str) { + StringBuffer output = new StringBuffer(); + new _StringLiteralEscape(output)..writeStringLiteral(str); + return output.toString(); +} + /// A collection of methods where all methods have the same name. /// This class is intended to optimize whether a specific invocation is /// appropritate for at least some of the methods in the collection. @@ -188,8 +296,8 @@ class _DeclarationSet { // Not enough positional arguments. return false; } - if (!_checkType( - invocation.positionalArguments[i], parameters[i].type)) return false; + if (!_checkType(invocation.positionalArguments[i], parameters[i].type)) + return false; } if (invocation.namedArguments.isNotEmpty) { var startNamed; @@ -208,8 +316,9 @@ class _DeclarationSet { for (var j = startNamed; j < parameters.length; j++) { var p = parameters[j]; if (p.simpleName == name) { - if (!_checkType(invocation.namedArguments[name], - parameters[j].type)) return false; + if (!_checkType( + invocation.namedArguments[name], parameters[j].type)) + return false; match = true; break; } @@ -345,7 +454,9 @@ bool hasDomName(mirrors.DeclarationMirror mirror) { _getJsMemberName(mirrors.DeclarationMirror mirror) { var name = _getJsName(mirror); - return name == null || name.isEmpty ? _stripReservedNamePrefix(_getDeclarationName(mirror)) : name; + return name == null || name.isEmpty + ? _stripReservedNamePrefix(_getDeclarationName(mirror)) + : name; } // TODO(jacobr): handle setters correctyl. @@ -369,7 +480,7 @@ String _accessJsPathHelper(Iterable parts) { ..write('${_JS_LIBRARY_PREFIX}.JsNative.getProperty(' * parts.length) ..write("${_JS_LIBRARY_PREFIX}.context"); for (var p in parts) { - sb.write(", '$p')"); + sb.write(", ${_escapeString(p)})"); } return sb.toString(); } @@ -380,13 +491,13 @@ String _accessJsPathHelper(Iterable parts) { String _accessJsPathSetter(String path) { var parts = path.split("."); return "${_JS_LIBRARY_PREFIX}.JsNative.setProperty(${_accessJsPathHelper(parts.getRange(0, parts.length - 1)) - }, '${parts.last}', v)"; + }, ${_escapeString(parts.last)}, v)"; } String _accessJsPathCallMethodHelper(String path) { var parts = path.split("."); return "${_JS_LIBRARY_PREFIX}.JsNative.callMethod(${_accessJsPathHelper(parts.getRange(0, parts.length - 1)) - }, '${parts.last}',"; + }, ${_escapeString(parts.last)},"; } @Deprecated("Internal Use Only") @@ -407,8 +518,7 @@ void addMemberHelper( } sb.write(" "); if (declaration.isGetter) { - sb.write( - "get $name => ${_accessJsPath(path)};"); + sb.write("get $name => ${_accessJsPath(path)};"); } else if (declaration.isSetter) { sb.write("set $name(v) {\n" " ${_JS_LIBRARY_PREFIX}.safeForTypedInterop(v);\n" @@ -471,7 +581,8 @@ bool _isExternal(mirrors.MethodMirror mirror) { return false; } -List _generateExternalMethods(List libraryPaths, bool useCachedPatches) { +List _generateExternalMethods( + List libraryPaths, bool useCachedPatches) { var staticCodegen = []; if (libraryPaths.length == 0) { @@ -489,7 +600,7 @@ List _generateExternalMethods(List libraryPaths, bool useCachedP // the patches for this file. _generateLibraryCodegen(uri, library, staticCodegen); } - }); // End of library foreach + }); // End of library foreach } else { // Used to generate cached_patches.dart file for all IDL generated dart: // files to the WebKit DOM. @@ -505,140 +616,140 @@ List _generateExternalMethods(List libraryPaths, bool useCachedP } _generateLibraryCodegen(uri, library, staticCodegen) { - // Is it a dart generated library? - var dartLibrary = uri.scheme == 'dart'; + // Is it a dart generated library? + var dartLibrary = uri.scheme == 'dart'; - var sb = new StringBuffer(); - String jsLibraryName = _getJsName(library); + var sb = new StringBuffer(); + String jsLibraryName = _getJsName(library); - // Sort by patch file by its declaration name. - var sortedDeclKeys = library.declarations.keys.toList(); - sortedDeclKeys.sort((a, b) => mirrors.MirrorSystem.getName(a).compareTo(mirrors.MirrorSystem.getName(b))); + // Sort by patch file by its declaration name. + var sortedDeclKeys = library.declarations.keys.toList(); + sortedDeclKeys.sort((a, b) => mirrors.MirrorSystem + .getName(a) + .compareTo(mirrors.MirrorSystem.getName(b))); - sortedDeclKeys.forEach((name) { - var declaration = library.declarations[name]; - if (declaration is mirrors.MethodMirror) { - if ((_hasJsName(declaration) || jsLibraryName != null) && - _isExternal(declaration)) { - addMemberHelper(declaration, jsLibraryName, sb); - } - } else if (declaration is mirrors.ClassMirror) { - mirrors.ClassMirror clazz = declaration; - var isDom = dartLibrary ? hasDomName(clazz) : false; - var isJsInterop = _hasJsName(clazz); - if (isDom || isJsInterop) { - // TODO(jacobr): verify class implements JavaScriptObject. - var className = mirrors.MirrorSystem.getName(clazz.simpleName); - bool isPrivate = className.startsWith('_'); - var classNameImpl = '${className}Impl'; - var sbPatch = new StringBuffer(); - if (isJsInterop) { - String jsClassName = _getJsMemberName(clazz); + sortedDeclKeys.forEach((name) { + var declaration = library.declarations[name]; + if (declaration is mirrors.MethodMirror) { + if ((_hasJsName(declaration) || jsLibraryName != null) && + _isExternal(declaration)) { + addMemberHelper(declaration, jsLibraryName, sb); + } + } else if (declaration is mirrors.ClassMirror) { + mirrors.ClassMirror clazz = declaration; + var isDom = dartLibrary ? hasDomName(clazz) : false; + var isJsInterop = _hasJsName(clazz); + if (isDom || isJsInterop) { + // TODO(jacobr): verify class implements JavaScriptObject. + var className = mirrors.MirrorSystem.getName(clazz.simpleName); + bool isPrivate = className.startsWith('_'); + var classNameImpl = '${className}Impl'; + var sbPatch = new StringBuffer(); + if (isJsInterop) { + String jsClassName = _getJsMemberName(clazz); - jsInterfaceTypes.add(clazz); - clazz.declarations.forEach((name, declaration) { - if (declaration is! mirrors.MethodMirror || - !_isExternal(declaration)) return; - if (declaration.isFactoryConstructor && - _isAnonymousClass(clazz)) { - sbPatch.write(" factory ${className}("); - int i = 0; - var args = []; - for (var p in declaration.parameters) { - args.add(mirrors.MirrorSystem.getName(p.simpleName)); - i++; - } - if (args.isNotEmpty) { - sbPatch - ..write('{') - ..write(args - .map((name) => '$name:${_UNDEFINED_VAR}') - .join(", ")) - ..write('}'); - } - sbPatch.write(") {\n" - " var ret = ${_JS_LIBRARY_PREFIX}.JsNative.newObject();\n"); - i = 0; - for (var p in declaration.parameters) { - assert(p.isNamed); // TODO(jacobr): throw. - var name = args[i]; - var jsName = _stripReservedNamePrefix( - mirrors.MirrorSystem.getName(p.simpleName)); - sbPatch.write(" if($name != ${_UNDEFINED_VAR}) {\n" - " ${_JS_LIBRARY_PREFIX}.safeForTypedInterop($name);\n" - " ${_JS_LIBRARY_PREFIX}.JsNative.setProperty(ret, '$jsName', $name);\n" - " }\n"); - i++; - } + jsInterfaceTypes.add(clazz); + clazz.declarations.forEach((name, declaration) { + if (declaration is! mirrors.MethodMirror || + !_isExternal(declaration)) return; + if (declaration.isFactoryConstructor && _isAnonymousClass(clazz)) { + sbPatch.write(" factory ${className}("); + int i = 0; + var args = []; + for (var p in declaration.parameters) { + args.add(mirrors.MirrorSystem.getName(p.simpleName)); + i++; + } + if (args.isNotEmpty) { + sbPatch + ..write('{') + ..write( + args.map((name) => '$name:${_UNDEFINED_VAR}').join(", ")) + ..write('}'); + } + sbPatch.write(") {\n" + " var ret = ${_JS_LIBRARY_PREFIX}.JsNative.newObject();\n"); + i = 0; + for (var p in declaration.parameters) { + assert(p.isNamed); // TODO(jacobr): throw. + var name = args[i]; + var jsName = _stripReservedNamePrefix( + mirrors.MirrorSystem.getName(p.simpleName)); + sbPatch.write(" if($name != ${_UNDEFINED_VAR}) {\n" + " ${_JS_LIBRARY_PREFIX}.safeForTypedInterop($name);\n" + " ${_JS_LIBRARY_PREFIX}.JsNative.setProperty(ret, ${_escapeString(jsName)}, $name);\n" + " }\n"); + i++; + } - sbPatch.write( - " return ret;" + sbPatch.write(" return ret;" "}\n"); - } else if (declaration.isConstructor || - declaration.isFactoryConstructor) { - sbPatch.write(" "); - addMemberHelper( - declaration, - (jsLibraryName != null && jsLibraryName.isNotEmpty) - ? "${jsLibraryName}.${jsClassName}" - : jsClassName, - sbPatch, - isStatic: true, - memberName: className); - } - }); // End of clazz.declarations.forEach + } else if (declaration.isConstructor || + declaration.isFactoryConstructor) { + sbPatch.write(" "); + addMemberHelper( + declaration, + (jsLibraryName != null && jsLibraryName.isNotEmpty) + ? "${jsLibraryName}.${jsClassName}" + : jsClassName, + sbPatch, + isStatic: true, + memberName: className); + } + }); // End of clazz.declarations.forEach - clazz.staticMembers.forEach((memberName, member) { - if (_isExternal(member)) { - sbPatch.write(" "); - addMemberHelper( - member, - (jsLibraryName != null && jsLibraryName.isNotEmpty) - ? "${jsLibraryName}.${jsClassName}" - : jsClassName, - sbPatch, - isStatic: true); - } - }); - } - if (isDom) { - sbPatch.write(" static Type get instanceRuntimeType => ${classNameImpl};\n"); - } - if (isPrivate) { - sb.write(""" + clazz.staticMembers.forEach((memberName, member) { + if (_isExternal(member)) { + sbPatch.write(" "); + addMemberHelper( + member, + (jsLibraryName != null && jsLibraryName.isNotEmpty) + ? "${jsLibraryName}.${jsClassName}" + : jsClassName, + sbPatch, + isStatic: true); + } + }); + } + if (isDom) { + sbPatch.write( + " static Type get instanceRuntimeType => ${classNameImpl};\n"); + } + if (isPrivate) { + sb.write(""" class ${escapePrivateClassPrefix}${className} implements $className {} """); - } + } - if (sbPatch.isNotEmpty) { - var typeVariablesClause = ''; - if (!clazz.typeVariables.isEmpty) { - typeVariablesClause = - '<${clazz.typeVariables.map((m) => mirrors.MirrorSystem.getName(m.simpleName)).join(',')}>'; - } - sb.write(""" + if (sbPatch.isNotEmpty) { + var typeVariablesClause = ''; + if (!clazz.typeVariables.isEmpty) { + typeVariablesClause = + '<${clazz.typeVariables.map((m) => mirrors.MirrorSystem.getName(m.simpleName)).join(',')}>'; + } + sb.write(""" patch class $className$typeVariablesClause { $sbPatch } """); - if (isDom) { - sb.write(""" + if (isDom) { + sb.write(""" class $classNameImpl$typeVariablesClause extends $className implements ${_JS_LIBRARY_PREFIX}.JSObjectInterfacesDom { ${classNameImpl}.internal_() : super.internal_(); get runtimeType => $className; toString() => super.toString(); } """); - } } } } - }); - if (sb.isNotEmpty) { - staticCodegen - ..add(uri.toString()) - ..add("${uri}_js_interop_patch.dart") - ..add(""" + } + }); + if (sb.isNotEmpty) { + staticCodegen + ..add(uri.toString()) + ..add("${uri}_js_interop_patch.dart") + ..add(""" import 'dart:js' as ${_JS_LIBRARY_PREFIX}; /** @@ -649,7 +760,7 @@ const ${_UNDEFINED_VAR} = const Object(); ${sb} """); - } + } } // Remember the @JS type to compare annotation type. @@ -679,11 +790,13 @@ void setupJsTypeCache() { * signal to generate and emit the patches to stdout to be captured and put into * the file sdk/lib/js/dartium/cached_patches.dart */ -List _generateInteropPatchFiles(List libraryPaths, genCachedPatches) { +List _generateInteropPatchFiles( + List libraryPaths, genCachedPatches) { // Cache the @JS Type. if (_atJsType == -1) setupJsTypeCache(); - var ret = _generateExternalMethods(libraryPaths, genCachedPatches ? false : true); + var ret = + _generateExternalMethods(libraryPaths, genCachedPatches ? false : true); var libraryPrefixes = new Map(); var prefixNames = new Set(); var sb = new StringBuffer(); @@ -720,8 +833,7 @@ List _generateInteropPatchFiles(List libraryPaths, genCachedPatc var className = mirrors.MirrorSystem.getName(typeMirror.simpleName); var isPrivate = className.startsWith('_'); if (isPrivate) className = '${escapePrivateClassPrefix}${className}'; - var fullName = - '${prefixName}.${className}'; + var fullName = '${prefixName}.${className}'; (isArray ? implementsArray : implements).add(fullName); if (!isArray && !isFunction && !isJSObject) { // For DOM classes we need to be a bit more conservative at tagging them @@ -960,25 +1072,27 @@ JsObject get context { _lookupType(o, bool isCrossFrame, bool isElement) { try { - var type = html_common.lookupType(o, isElement); - var typeMirror = mirrors.reflectType(type); - var legacyInteropConvertToNative = typeMirror.isSubtypeOf(mirrors.reflectType(html.Blob)) || - typeMirror.isSubtypeOf(mirrors.reflectType(html.Event)) || - typeMirror.isSubtypeOf(mirrors.reflectType(indexed_db.KeyRange)) || - typeMirror.isSubtypeOf(mirrors.reflectType(html.ImageData)) || - typeMirror.isSubtypeOf(mirrors.reflectType(html.Node)) || + var type = html_common.lookupType(o, isElement); + var typeMirror = mirrors.reflectType(type); + var legacyInteropConvertToNative = + typeMirror.isSubtypeOf(mirrors.reflectType(html.Blob)) || + typeMirror.isSubtypeOf(mirrors.reflectType(html.Event)) || + typeMirror.isSubtypeOf(mirrors.reflectType(indexed_db.KeyRange)) || + typeMirror.isSubtypeOf(mirrors.reflectType(html.ImageData)) || + typeMirror.isSubtypeOf(mirrors.reflectType(html.Node)) || // TypedData is removed from this list as it is converted directly // rather than flowing through the interceptor code path. // typeMirror.isSubtypeOf(mirrors.reflectType(typed_data.TypedData)) || - typeMirror.isSubtypeOf(mirrors.reflectType(html.Window)); - if (isCrossFrame && !typeMirror.isSubtypeOf(mirrors.reflectType(html.Window))) { + typeMirror.isSubtypeOf(mirrors.reflectType(html.Window)); + if (isCrossFrame && + !typeMirror.isSubtypeOf(mirrors.reflectType(html.Window))) { // TODO(jacobr): evaluate using the true cross frame Window class, etc. // as well as triggering that legacy JS Interop returns raw JsObject // instances. legacyInteropConvertToNative = false; } return [type, legacyInteropConvertToNative]; - } catch (e) { } + } catch (e) {} return [JSObject.instanceRuntimeType, false]; } @@ -1060,7 +1174,8 @@ class JsObject extends _JSObjectBase { static JsObject _jsify(object) native "JsObject_jsify"; - static JsObject _fromBrowserObject(object) native "JsObject_fromBrowserObject"; + static JsObject _fromBrowserObject(object) + native "JsObject_fromBrowserObject"; /** * Returns the value associated with [property] from the proxied JavaScript @@ -1103,8 +1218,7 @@ class JsObject extends _JSObjectBase { return _identityEquality(this, other); } - static bool _identityEquality(a, b) - native "JsObject_identityEquality"; + static bool _identityEquality(a, b) native "JsObject_identityEquality"; /** * Returns `true` if the JavaScript object contains the specified property @@ -1163,7 +1277,6 @@ class JsObject extends _JSObjectBase { _callMethodLegacy(String name, List args) native "JsObject_callMethodLegacy"; } - /// Base class for all JS objects used through dart:html and typed JS interop. @Deprecated("Internal Use Only") class JSObject extends _JSObjectBase { @@ -1200,8 +1313,8 @@ class JSObject extends _JSObjectBase { if (matches != null) return ret; if (ret is Function || (ret is JsFunction /* shouldn't be needed in the future*/) && - _allowedMethods.containsKey( - invocation.memberName)) return ret; // Warning: we have not bound "this"... we could type check on the Function but that is of little value in Dart. + _allowedMethods.containsKey(invocation.memberName)) + return ret; // Warning: we have not bound "this"... we could type check on the Function but that is of little value in Dart. throwError(); } else { // TODO(jacobr): should we throw if the JavaScript object doesn't have the property? @@ -1210,8 +1323,8 @@ class JSObject extends _JSObjectBase { } else if (invocation.isSetter) { if (CHECK_JS_INVOCATIONS) { var matches = _allowedSetters[invocation.memberName]; - if (matches == null || - !matches.checkInvocation(invocation)) throwError(); + if (matches == null || !matches.checkInvocation(invocation)) + throwError(); } assert(name.endsWith("=")); name = name.substring(0, name.length - 1); @@ -1221,8 +1334,8 @@ class JSObject extends _JSObjectBase { var matches; if (CHECK_JS_INVOCATIONS) { matches = _allowedMethods[invocation.memberName]; - if (matches == null || - !matches.checkInvocation(invocation)) throwError(); + if (matches == null || !matches.checkInvocation(invocation)) + throwError(); } var ret = _callMethod(name, _buildArgs(invocation)); if (CHECK_JS_INVOCATIONS) { @@ -1302,7 +1415,8 @@ class JSFunction extends JSObject implements Function { a8 = _UNDEFINED, a9 = _UNDEFINED, a10 = _UNDEFINED]) { - return _apply(_stripUndefinedArgs([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10])); + return _apply( + _stripUndefinedArgs([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10])); } noSuchMethod(Invocation invocation) { @@ -1314,7 +1428,8 @@ class JSFunction extends JSObject implements Function { dynamic _apply(List args, {thisArg}) native "JSFunction_apply"; - static JSFunction _createWithThis(Function f) native "JSFunction_createWithThis"; + static JSFunction _createWithThis(Function f) + native "JSFunction_createWithThis"; static JSFunction _create(Function f) native "JSFunction_create"; } @@ -1329,11 +1444,16 @@ class JsNative { static hasProperty(_JSObjectBase o, name) => o._hasProperty(name); static getProperty(_JSObjectBase o, name) => o._operator_getter(name); - static setProperty(_JSObjectBase o, name, value) => o._operator_setter(name, value); - static callMethod(_JSObjectBase o, String method, List args) => o._callMethod(method, args); - static instanceof(_JSObjectBase o, /*JsFunction|JSFunction*/ type) => o._instanceof(type); - static callConstructor0(_JSObjectBase constructor) native "JSNative_callConstructor0"; - static callConstructor(_JSObjectBase constructor, List args) native "JSNative_callConstructor"; + static setProperty(_JSObjectBase o, name, value) => + o._operator_setter(name, value); + static callMethod(_JSObjectBase o, String method, List args) => + o._callMethod(method, args); + static instanceof(_JSObjectBase o, /*JsFunction|JSFunction*/ type) => + o._instanceof(type); + static callConstructor0(_JSObjectBase constructor) + native "JSNative_callConstructor0"; + static callConstructor(_JSObjectBase constructor, List args) + native "JSNative_callConstructor"; static toTypedObject(JsObject o) native "JSNative_toTypedObject"; @@ -1344,42 +1464,6 @@ class JsNative { static JSFunction withThis(Function f) native "JsFunction_withThisNoWrap"; } -/// Utility methods to efficiently manipulate typed JSInterop objects in cases -/// where the name to call is not known at runtime. You should only use these -/// methods when the same effect cannot be achieved with @JS annotations. -/// These methods would be extension methods on JSObject if Dart supported -/// extension methods. -class JSNative { - /** - * WARNING: performance of this method is much worse than other methods - * in JSNative. Only use this method as a last resort. - * - * Recursively converts a JSON-like collection of Dart objects to a - * collection of JavaScript objects and returns a [JsObject] proxy to it. - * - * [object] must be a [Map] or [Iterable], the contents of which are also - * converted. Maps and Iterables are copied to a new JavaScript object. - * Primitives and other transferrable values are directly converted to their - * JavaScript type, and all other objects are proxied. - */ - static jsify(object) { - if ((object is! Map) && (object is! Iterable)) { - throw new ArgumentError("object must be a Map or Iterable"); - } - return _jsify(object); - } - - static _jsify(object) native "JSObject_jsify"; - static JSObject newObject() native "JSObject_newObject"; - - static hasProperty(JSObject o, name) => o._hasProperty(name); - static getProperty(JSObject o, name) => o._operator_getter(name); - static setProperty(JSObject o, name, value) => o._operator_setter(name, value); - static callMethod(JSObject o, String method, List args) => o._callMethod(method, args); - static instanceof(JSObject o, Function type) => o._instanceof(type); - static callConstructor(JSObject constructor, List args) native "JSNative_callConstructor"; -} - /** * Proxies a JavaScript Function object. */ @@ -1396,8 +1480,7 @@ 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}) => - _apply(args, thisArg: thisArg); + dynamic apply(List args, {thisArg}) => _apply(args, thisArg: thisArg); dynamic _apply(List args, {thisArg}) native "JsFunction_apply"; @@ -1578,7 +1661,7 @@ Function _wrapAsDebuggerVarArgsFunction(JsFunction jsFunction) => ( /// JavaScript. We may remove the need to call this method completely in the /// future if Dart2Js is refactored so that its function calling conventions /// are more compatible with JavaScript. -Function /*=F*/ allowInterop/**/(Function /*=F*/ f) { +Function/*=F*/ allowInterop/**/(Function/*=F*/ f) { if (f is JSFunction) { // The function is already a JSFunction... no need to do anything. return f; diff --git a/sdk/lib/js_util/dart2js/js_util_dart2js.dart b/sdk/lib/js_util/dart2js/js_util_dart2js.dart new file mode 100644 index 00000000000..da9b3677956 --- /dev/null +++ b/sdk/lib/js_util/dart2js/js_util_dart2js.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2016, 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. + +/// Utility methods to efficiently manipulate typed JSInterop objects in cases +/// where the name to call is not known at runtime. You should only use these +/// methods when the same effect cannot be achieved with @JS annotations. +/// These methods would be extension methods on JSObject if Dart supported +/// extension methods. +library dart.js_util; + +import 'dart:_foreign_helper' show JS; +import 'dart:collection' show HashMap; + +/// WARNING: performance of this method is much worse than other uitil +/// methods in this library. Only use this method as a last resort. +/// +/// Recursively converts a JSON-like collection of Dart objects to a +/// collection of JavaScript objects and returns a [JsObject] proxy to it. +/// +/// [object] must be a [Map] or [Iterable], the contents of which are also +/// converted. Maps and Iterables are copied to a new JavaScript object. +/// Primitives and other transferrable values are directly converted to their +/// JavaScript type, and all other objects are proxied. +jsify(object) { + if ((object is! Map) && (object is! Iterable)) { + throw new ArgumentError("object must be a Map or Iterable"); + } + return _convertDataTree(object); +} + +_convertDataTree(data) { + var _convertedObjects = new HashMap.identity(); + + _convert(o) { + if (_convertedObjects.containsKey(o)) { + return _convertedObjects[o]; + } + if (o is Map) { + final convertedMap = JS('=Object', '{}'); + _convertedObjects[o] = convertedMap; + for (var key in o.keys) { + JS('=Object', '#[#]=#', convertedMap, key, _convert(o[key])); + } + return convertedMap; + } else if (o is Iterable) { + var convertedList = []; + _convertedObjects[o] = convertedList; + convertedList.addAll(o.map(_convert)); + return convertedList; + } else { + return o; + } + } + + return _convert(data); +} + +JSObject newObject() => JS('=Object', '{}'); + +hasProperty(o, name) => JS('bool', '# in #', name, o); +getProperty(o, name) => JS('Object', '#[#]', o, name); +setProperty(o, name, value) => JS('', '#[#]=#', o, name, value); + +callMethod(o, String method, List args) => + JS('Object', '#[#].apply(#, #)', o, method, o, args); + +instanceof(o, Function type) => JS('bool', '# instanceof #', o, type); +callConstructor(Function constr, List arguments) { + if (arguments == null) { + return JS('Object', 'new #()', constr); + } + + if (JS('bool', '# instanceof Array', arguments)) { + int argumentCount = JS('int', '#.length', arguments); + switch (argumentCount) { + case 0: + return JS('Object', 'new #()', constr); + + case 1: + var arg0 = JS('', '#[0]', arguments); + return JS('Object', 'new #(#)', constr, arg0); + + case 2: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + return JS('Object', 'new #(#, #)', constr, arg0, arg1); + + case 3: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + var arg2 = JS('', '#[2]', arguments); + return JS('Object', 'new #(#, #, #)', constr, arg0, arg1, arg2); + + case 4: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + var arg2 = JS('', '#[2]', arguments); + var arg3 = JS('', '#[3]', arguments); + return JS( + 'Object', 'new #(#, #, #, #)', constr, arg0, arg1, arg2, arg3); + } + } + + // The following code solves the problem of invoking a JavaScript + // constructor with an unknown number arguments. + // First bind the constructor to the argument list using bind.apply(). + // The first argument to bind() is the binding of 't', so add 'null' to + // the arguments list passed to apply(). + // After that, use the JavaScript 'new' operator which overrides any binding + // of 'this' with the new instance. + var args = [null]..addAll(arguments); + var factoryFunction = JS('', '#.bind.apply(#, #)', constr, constr, args); + // Without this line, calling factoryFunction as a constructor throws + JS('String', 'String(#)', factoryFunction); + // This could return an UnknownJavaScriptObject, or a native + // object for which there is an interceptor + return JS('Object', 'new #()', factoryFunction); + + // TODO(sra): Investigate: + // + // var jsObj = JS('', 'Object.create(#.prototype)', constr); + // JS('', '#.apply(#, #)', constr, jsObj, + // []..addAll(arguments.map(_convertToJS))); + // return _wrapToDart(jsObj); +} diff --git a/sdk/lib/js_util/dartium/js_util_dartium.dart b/sdk/lib/js_util/dartium/js_util_dartium.dart new file mode 100644 index 00000000000..5f1226d6673 --- /dev/null +++ b/sdk/lib/js_util/dartium/js_util_dartium.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2016, 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. + +/// Utility methods to efficiently manipulate typed JSInterop objects in cases +/// where the name to call is not known at runtime. You should only use these +/// methods when the same effect cannot be achieved with @JS annotations. +/// These methods would be extension methods on JSObject if Dart supported +/// extension methods. +library dart.js_util; + +import 'dart:js'; + +/// WARNING: performance of this method is much worse than other uitil +/// methods in this library. Only use this method as a last resort. +/// +/// Recursively converts a JSON-like collection of Dart objects to a +/// collection of JavaScript objects and returns a [JsObject] proxy to it. +/// +/// [object] must be a [Map] or [Iterable], the contents of which are also +/// converted. Maps and Iterables are copied to a new JavaScript object. +/// Primitives and other transferrable values are directly converted to their +/// JavaScript type, and all other objects are proxied. +jsify(object) { + if ((object is! Map) && (object is! Iterable)) { + throw new ArgumentError("object must be a Map or Iterable"); + } + return JsNative.jsify(object); +} + +JSObject newObject() => JsNative.newObject(); + +hasProperty(JSObject o, name) => JsNative.hasProperty(o, name); +getProperty(JSObject o, name) => JsNative.getProperty(o, name); +setProperty(JSObject o, name, value) => JsNative.setProperty(o, name, value); +callMethod(JSObject o, String method, List args) => JsNative.callMethod(o, method, args); +instanceof(JSObject o, Function type) => JsNative.instanceof(o, type); +callConstructor(JSObject constructor, List args) => JsNative.callConstructor(constructor, args); diff --git a/tests/compiler/dart2js_native/call_on_native_class_test.dart b/tests/compiler/dart2js_native/call_on_native_class_test.dart deleted file mode 100644 index 51429821bf3..00000000000 --- a/tests/compiler/dart2js_native/call_on_native_class_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -import 'dart:_js_helper'; - -@Native('*A') -class A { -} - -class B extends A { - call() => 42; -} - -main() { - new B()(); -} diff --git a/tests/compiler/dart2js_native/dart2js_native.status b/tests/compiler/dart2js_native/dart2js_native.status index e1d90fdbc5c..84bc7b33182 100644 --- a/tests/compiler/dart2js_native/dart2js_native.status +++ b/tests/compiler/dart2js_native/dart2js_native.status @@ -5,8 +5,6 @@ [ $browser ] *: Skip -[ $compiler == dart2js ] -call_on_native_class_test: RuntimeError # Issue 14813 native_no_such_method_exception4_frog_test: CompileTimeError # Issue 9631 native_no_such_method_exception5_frog_test: CompileTimeError # Issue 9631 diff --git a/tests/html/js_typed_interop_test.dart b/tests/html/js_typed_interop_test.dart index 431d533037e..960c0ccf453 100644 --- a/tests/html/js_typed_interop_test.dart +++ b/tests/html/js_typed_interop_test.dart @@ -71,6 +71,15 @@ _injectJs() { getA: function() { return this.a;} }; + function _PrivateClass(a, b) { + this._a = a; + this._b = b; + }; + + _PrivateClass.prototype = { + _getA: function() { return this._a;} + }; + var selection = ["a", "b", "c", foo, bar]; function returnNumArgs() { return arguments.length; }; @@ -78,6 +87,23 @@ _injectJs() { function confuse(obj) { return obj; } + window['class'] = function() { return 42; }; + window['delete'] = 100; + window['JS$hasJsInName'] = 'unicorn'; + window['JS$hasJsInNameMethod'] = function(x) { return x*5; }; + + function JS$ClassWithJSInName(x) { + this.x = x; + this.JS$hasJsInName = 73; + this.$JS$doesNotNeedEscape = 103; + }; + + JS$ClassWithJSInName.prototype = { + JS$getXwithJsInName: function() { return this.x;} + }; + + JS$ClassWithJSInName.JS$staticMethod = function(x) { return x * 3; }; + function StringWrapper(str) { this.str = str; } @@ -110,6 +136,49 @@ class ClassWithConstructor { external get b; } +@JS('ClassWithConstructor') +class _ClassWithConstructor { + external _ClassWithConstructor(aParam, bParam); + external getA(); + external get a; + external get b; +} + +@JS() +class JS$_PrivateClass { + external JS$_PrivateClass(aParam, bParam); + external JS$_getA(); + external get JS$_a; + external get JS$_b; + // Equivalent to JS$_a but only visible within + // the class. + external get _a; +} + +@JS() +external String get JS$JS$hasJsInName; + +@JS() +external int JS$JS$hasJsInNameMethod(int x); + +// This is the prefered way to handle static or top level members that start +// with JS$. We verify that JS$JS$ works purely to prevent bugs. +@JS(r'JS$hasJsInName') +external String get JS$hasJsInName; + +@JS(r'JS$hasJsInNameMethod') +external int JS$hasJsInNameMethod(int x); + +@JS() +class JS$JS$ClassWithJSInName { + external JS$JS$ClassWithJSInName(x); + external int get x; + external int get JS$JS$hasJsInName; + external int get $JS$doesNotNeedEscape; + external int JS$JS$getXwithJsInName(); + external static int JS$JS$staticMethod(x); +} + typedef num MultiplyWithDefault(num a, [num b]); @JS() @@ -118,6 +187,7 @@ class Foo { external set x(int v); external num multiplyByX(num y); external num multiplyBy2(num y); + external num JS$multiplyBy2(num y); external MultiplyWithDefault get multiplyDefault2Function; external callClosureWithArgAndThis(Function closure, arg); @@ -126,14 +196,17 @@ class Foo { external Bar getBar(); external static num multiplyDefault2(num a, [num b]); + // Should desugar to multiplyDefault2. + external static num JS$multiplyDefault2(num a, [num b]); } @anonymous @JS() class ExampleLiteral { - external factory ExampleLiteral({int x, String y, num z}); + external factory ExampleLiteral({int x, String y, num z, JS$class}); external int get x; + external int get JS$class; external String get y; external num get z; } @@ -182,6 +255,13 @@ class StringWrapper { @JS() external confuse(obj); +/// Desugars to calling the js method named class. +@JS() +external JS$class(); + +@JS() +external get JS$delete; + @JS() external CanvasRenderingContext2D getCanvasContext(); @@ -191,6 +271,16 @@ external num get propertyOnDocument; @JS('window.self.window.window.windowProperty') external num get propertyOnWindow; +@JS() +@anonymous +class Simple +{ + external List get numbers; + external set numbers(List numbers); + + external factory Simple({ List numbers }); +} + main() { _injectJs(); @@ -210,6 +300,17 @@ main() { expect(stringify(l), equals('{"z":100}')); }); + test('with array', () { + // Repro for https://github.com/dart-lang/sdk/issues/26768 + var simple = new Simple(numbers: [ 1, 2, 3 ]); + expect(stringify(simple), equals('{"numbers":[1,2,3]}')); + }); + + test(r'JS$ escaped name', () { + var l = new ExampleLiteral(JS$class: 3, y: "foo"); + expect(l.JS$class, equals(3)); + }); + test('empty', () { var l = new EmptyLiteral(); expect(stringify(l), equals('{}')); @@ -225,6 +326,25 @@ main() { }); }); + group('private class', () { + test('simple', () { + var o = new _ClassWithConstructor("foo", "bar"); + expect(o.a, equals("foo")); + expect(o.b, equals("bar")); + expect(o.getA(), equals("foo")); + }); + }); + + group('private class', () { + test('simple', () { + var o = new JS$_PrivateClass("foo", "bar"); + expect(o.JS$_a, equals("foo")); + expect(o.JS$_b, equals("bar")); + expect(o._a, equals("foo")); + expect(o.JS$_getA(), equals("foo")); + }); + }); + group('property', () { test('get', () { expect(foo.x, equals(3)); @@ -276,6 +396,22 @@ main() { expect(untypedFunction(), isNaN); }); + + test(r'JS$ escaped name', () { + foo.x = 10; + expect(foo.JS$multiplyBy2(5), equals(10)); + + Function multiplyBy2 = foo.JS$multiplyBy2; + expect(multiplyBy2(5), equals(10)); + }); + + test(r'JS$ double escaped name', () { + var obj = new JS$JS$ClassWithJSInName(42); + expect(obj.x, equals(42)); + expect(obj.JS$JS$getXwithJsInName(), equals(42)); + expect(obj.JS$JS$hasJsInName, equals(73)); + expect(obj.$JS$doesNotNeedEscape, equals(103)); + }); }); group('static_method_call', () { @@ -283,6 +419,15 @@ main() { expect(Foo.multiplyDefault2(6, 7), equals(42)); expect(Foo.multiplyDefault2(6), equals(12)); }); + + test(r'JS$ escaped name', () { + expect(Foo.JS$multiplyDefault2(6, 7), equals(42)); + expect(Foo.JS$multiplyDefault2(6), equals(12)); + }); + + test(r'JS$ double escaped name', () { + expect(JS$JS$ClassWithJSInName.JS$JS$staticMethod(4), equals(12)); + }); }); // Note: these extra groups are added to be able to mark each test @@ -384,6 +529,17 @@ main() { }); }); + group(r'JS$ escaped', () { + test('top level', () { + expect(JS$class(), equals(42)); + expect(JS$delete, equals(100)); + }); + test('top level double escaped', () { + expect(JS$JS$hasJsInName, equals('unicorn')); + expect(JS$JS$hasJsInNameMethod(4), equals(20)); + }); + }); + group('type check', () { test('js interfaces', () { // Is checks return true for all JavaScript interfaces. diff --git a/tests/html/js_util_test.dart b/tests/html/js_util_test.dart new file mode 100644 index 00000000000..b58e0aac059 --- /dev/null +++ b/tests/html/js_util_test.dart @@ -0,0 +1,347 @@ +// 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. + +@JS() +library js_native_test; + +import 'dart:async'; +import 'dart:html'; +import 'dart:typed_data' show ByteBuffer, Int32List; +import 'dart:indexed_db' show IdbFactory, KeyRange; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; + +import 'package:unittest/unittest.dart'; +import 'package:unittest/html_individual_config.dart'; + +_injectJs() { + final script = new ScriptElement(); + script.type = 'text/javascript'; + script.innerHtml = r""" +var x = 42; + +var _x = 123; + +var myArray = ["value1"]; + +function returnThis() { + return this; +} + +function getTypeOf(o) { + return typeof(o); +} + +function Foo(a) { + this.a = a; +} + +Foo.b = 38; + +Foo.prototype.bar = function() { + return this.a; +} +Foo.prototype.toString = function() { + return "I'm a Foo a=" + this.a; +} + +var container = new Object(); +container.Foo = Foo; + +function checkMap(m, key, value) { + if (m.hasOwnProperty(key)) + return m[key] == value; + else + return false; +} + +"""; + document.body.append(script); +} + +@JS() +external bool checkMap(m, String, value); + +@JS('JSON.stringify') +external String stringify(o); + +@JS('Node') +external Function get JSNodeType; + +@JS('Element') +external Function get JSElementType; + +@JS('Text') +external Function get JSTextType; + +@JS('HTMLCanvasElement') +external Function get JSHtmlCanvasElementType; + +@JS() +class Foo { + external Foo(num a); + + external num get a; + external num bar(); +} + +@JS('Foo') +external Function get JSFooType; + +@JS() +@anonymous +class ExampleTypedLiteral { + external factory ExampleTypedLiteral({a, b, JS$_c, JS$class}); + + external get a; + external get b; + external get JS$_c; + external set JS$_c(v); + // Identical to JS$_c but only accessible within the library. + external get _c; + external get JS$class; + external set JS$class(v); +} + +@JS("Object.prototype.hasOwnProperty") +external Function get _hasOwnProperty; + +bool hasOwnProperty(o, String name) { + return js_util.callMethod(_hasOwnProperty, 'call', [o, name]); +} + +main() { + _injectJs(); + useHtmlIndividualConfiguration(); + + group('js_util.jsify()', () { + test('convert a List', () { + final list = [1, 2, 3, 4, 5, 6, 7, 8]; + var array = js_util.jsify(list); + expect(array is List, isTrue); + expect(identical(array, list), isFalse); + expect(array.length, equals(list.length)); + for (var i = 0; i < list.length; i++) { + expect(array[i], equals(list[i])); + } + }); + + test('convert an Iterable', () { + final set = new Set.from([1, 2, 3, 4, 5, 6, 7, 8]); + var array = js_util.jsify(set); + expect(array is List, isTrue); + expect(array.length, equals(set.length)); + for (var i = 0; i < array.length; i++) { + expect(set.contains(array[i]), isTrue); + } + }); + + test('convert a Map', () { + var map = {'a': 1, 'b': 2, 'c': 3}; + var jsMap = js_util.jsify(map); + expect(jsMap is! List, isTrue); + for (var key in map.keys) { + expect(checkMap(jsMap, key, map[key]), isTrue); + } + }); + + test('deep convert a complex object', () { + final object = { + 'a': [ + 1, + [2, 3] + ], + 'b': {'c': 3, 'd': new Foo(42)}, + 'e': null + }; + var jsObject = js_util.jsify(object); + expect(js_util.getProperty(jsObject, 'a')[0], equals(object['a'][0])); + expect( + js_util.getProperty(jsObject, 'a')[1][0], equals(object['a'][1][0])); + expect( + js_util.getProperty(jsObject, 'a')[1][1], equals(object['a'][1][1])); + expect(js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'c'), + equals(object['b']['c'])); + expect(js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'd'), + equals(object['b']['d'])); + expect( + js_util.callMethod( + js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'd'), + 'bar', []), + equals(42)); + expect(js_util.getProperty(jsObject, 'e'), isNull); + }); + + test('throws if object is not a Map or Iterable', () { + expect(() => js_util.jsify('a'), + throwsA(new isInstanceOf())); + }); + }); + + group('js_util.newObject', () { + test('create', () { + expect(identical(js_util.newObject(), js_util.newObject()), isFalse); + }); + + test('callMethod', () { + var o = js_util.newObject(); + expect(js_util.callMethod(o, 'toString', []), equals('[object Object]')); + expect(stringify(o), equals('{}')); + }); + + test('properties', () { + var o = js_util.newObject(); + expect(js_util.hasProperty(o, 'foo bar'), isFalse); + expect(js_util.hasProperty(o, 'toString'), isTrue); + expect(hasOwnProperty(o, 'toString'), isFalse); + expect(hasOwnProperty(o, 'foo bar'), isFalse); + js_util.setProperty(o, 'foo bar', 42); + expect(hasOwnProperty(o, 'foo bar'), isTrue); + expect(js_util.getProperty(o, 'foo bar'), equals(42)); + expect(js_util.hasProperty(o, 'foo bar'), isTrue); + expect(stringify(o), equals('{"foo bar":42}')); + }); + }); + + group('hasProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.hasProperty(f, 'a'), isTrue); + expect(js_util.hasProperty(f, 'toString'), isTrue); + js_util.setProperty(f, '__proto__', null); + expect(js_util.hasProperty(f, 'toString'), isFalse); + }); + test('typed literal', () { + var l = + new ExampleTypedLiteral(a: 'x', b: 42, JS$_c: null, JS$class: true); + expect(js_util.hasProperty(l, 'a'), isTrue); + expect(js_util.hasProperty(l, 'b'), isTrue); + expect(js_util.hasProperty(l, '_c'), isTrue); + expect(l.JS$_c, isNull); + expect(js_util.hasProperty(l, 'class'), isTrue); + // JS$_c escapes to _c so the property JS$_c will not exist on the object. + expect(js_util.hasProperty(l, r'JS$_c'), isFalse); + expect(js_util.hasProperty(l, r'JS$class'), isFalse); + expect(l.JS$class, isTrue); + + l = new ExampleTypedLiteral(a: null); + expect(js_util.hasProperty(l, 'a'), isTrue); + expect(js_util.hasProperty(l, 'b'), isFalse); + expect(js_util.hasProperty(l, '_c'), isFalse); + expect(js_util.hasProperty(l, 'class'), isFalse); + + l = new ExampleTypedLiteral(JS$_c: 74); + expect(js_util.hasProperty(l, '_c'), isTrue); + expect(l.JS$_c, equals(74)); + }); + }); + + group('getProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.getProperty(f, 'a'), equals(42)); + expect(js_util.getProperty(f, 'toString') is Function, isTrue); + js_util.setProperty(f, '__proto__', null); + expect(js_util.getProperty(f, 'toString'), isNull); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(a: 'x', b: 42, JS$_c: 7, JS$class: true); + expect(js_util.getProperty(l, 'a'), equals('x')); + expect(js_util.getProperty(l, 'b'), equals(42)); + expect(js_util.getProperty(l, '_c'), equals(7)); + expect(l.JS$_c, equals(7)); + expect(js_util.getProperty(l, 'class'), isTrue); + expect(js_util.getProperty(l, r'JS$_c'), isNull); + expect(js_util.getProperty(l, r'JS$class'), isNull); + }); + }); + + group('setProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.getProperty(f, 'a'), equals(42)); + js_util.setProperty(f, 'a', 100); + expect(f.a, equals(100)); + expect(js_util.getProperty(f, 'a'), equals(100)); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(); + js_util.setProperty(l, 'a', 'foo'); + expect(js_util.getProperty(l, 'a'), equals('foo')); + expect(l.a, equals('foo')); + js_util.setProperty(l, 'a', l); + expect(identical(l.a, l), isTrue); + var list = ['arr']; + js_util.setProperty(l, 'a', list); + expect(identical(l.a, list), isTrue); + l.JS$class = 42; + expect(l.JS$class, equals(42)); + js_util.setProperty(l, 'class', 100); + expect(l.JS$class, equals(100)); + }); + }); + + group('callMethod', () { + test('html object', () { + var canvas = new Element.tag('canvas'); + expect( + identical(canvas.getContext('2d'), + js_util.callMethod(canvas, 'getContext', ['2d'])), + isTrue); + }); + + test('typed object', () { + var f = new Foo(42); + expect(js_util.callMethod(f, 'bar', []), equals(42)); + }); + }); + + group('instanceof', () { + test('html object', () { + var canvas = new Element.tag('canvas'); + expect(js_util.instanceof(canvas, JSNodeType), isTrue); + expect(js_util.instanceof(canvas, JSTextType), isFalse); + expect(js_util.instanceof(canvas, JSElementType), isTrue); + expect(js_util.instanceof(canvas, JSHtmlCanvasElementType), isTrue); + var div = new Element.tag('div'); + expect(js_util.instanceof(div, JSNodeType), isTrue); + expect(js_util.instanceof(div, JSTextType), isFalse); + expect(js_util.instanceof(div, JSElementType), isTrue); + expect(js_util.instanceof(div, JSHtmlCanvasElementType), isFalse); + + var text = new Text('foo'); + expect(js_util.instanceof(text, JSNodeType), isTrue); + expect(js_util.instanceof(text, JSTextType), isTrue); + expect(js_util.instanceof(text, JSElementType), isFalse); + }); + + test('typed object', () { + var f = new Foo(42); + expect(js_util.instanceof(f, JSFooType), isTrue); + expect(js_util.instanceof(f, JSNodeType), isFalse); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(); + expect(js_util.instanceof(l, JSFooType), isFalse); + }); + }); + + group('callConstructor', () { + test('html object', () { + var textNode = js_util.callConstructor(JSTextType, ['foo']); + expect(js_util.instanceof(textNode, JSTextType), isTrue); + expect(textNode is Text, isTrue); + expect(textNode.text, equals('foo')); + }); + + test('typed object', () { + Foo f = js_util.callConstructor(JSFooType, [42]); + expect(f.a, equals(42)); + }); + }); +} diff --git a/tools/create_sdk.py b/tools/create_sdk.py index 65f585e055f..dad929bdba1 100755 --- a/tools/create_sdk.py +++ b/tools/create_sdk.py @@ -59,6 +59,7 @@ # ......io/ # ......isolate/ # ......js/ +# ......js_util/ # ......math/ # ......mirrors/ # ......typed_data/ @@ -254,7 +255,7 @@ def Main(): join('html', 'dart2js'), join('html', 'dartium'), join('html', 'html_common'), join('indexed_db', 'dart2js'), join('indexed_db', 'dartium'), - 'js', 'math', 'mirrors', 'profiler', 'typed_data', + 'js', 'js_util', 'math', 'mirrors', 'profiler', 'typed_data', join('svg', 'dart2js'), join('svg', 'dartium'), join('web_audio', 'dart2js'), join('web_audio', 'dartium'), join('web_gl', 'dart2js'), join('web_gl', 'dartium'),