From c1a2175904167a470979dc14cec9264d960678ca Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Sat, 18 Mar 2017 11:40:12 +0900 Subject: [PATCH] fix(toString): fix #666, Zone patched method toString should like before patched --- lib/browser/browser.ts | 4 ++++ lib/browser/register-element.ts | 4 +++- lib/common/to-string.ts | 36 +++++++++++++++++++++++++++++++++ lib/common/utils.ts | 18 ++++++++++++++++- lib/node/node.ts | 4 ++++ test/common/toString.spec.ts | 33 ++++++++++++++++++++++++++++++ test/common_tests.ts | 1 + 7 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 lib/common/to-string.ts create mode 100644 test/common/toString.spec.ts diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 785e0b86a..06e66eeab 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -7,6 +7,7 @@ */ import {patchTimer} from '../common/timers'; +import {patchFuncToString} from '../common/to-string'; import {findEventTask, patchClass, patchEventTargetMethods, patchMethod, patchPrototype, zoneSymbol} from '../common/utils'; import {propertyPatch} from './define-property'; @@ -151,6 +152,9 @@ if (_global['navigator'] && _global['navigator'].geolocation) { patchPrototype(_global['navigator'].geolocation, ['getCurrentPosition', 'watchPosition']); } +// patch Func.prototype.toString to let them look like native +patchFuncToString(); + // handle unhandled promise rejection function findPromiseRejectionHandler(evtName: string) { return function(e: any) { diff --git a/lib/browser/register-element.ts b/lib/browser/register-element.ts index 153a7c6d8..0f6f9a49a 100644 --- a/lib/browser/register-element.ts +++ b/lib/browser/register-element.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {isBrowser, isMix} from '../common/utils'; +import {attachOriginToPatched, isBrowser, isMix} from '../common/utils'; import {_redefineProperty} from './define-property'; @@ -39,4 +39,6 @@ export function registerElementPatch(_global: any) { return _registerElement.apply(document, [name, opts]); }; + + attachOriginToPatched((document).registerElement, _registerElement); } diff --git a/lib/common/to-string.ts b/lib/common/to-string.ts new file mode 100644 index 000000000..2a4fec3ce --- /dev/null +++ b/lib/common/to-string.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {zoneSymbol} from './utils'; + +// override Function.prototype.toString to make zone.js patched function +// look like native function +export function patchFuncToString() { + const originalFunctionToString = Function.prototype.toString; + const g: any = + typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global; + Function.prototype.toString = function() { + if (typeof this === 'function') { + if (this[zoneSymbol('OriginalDelegate')]) { + return originalFunctionToString.apply(this[zoneSymbol('OriginalDelegate')], arguments); + } + if (this === Promise) { + const nativePromise = g[zoneSymbol('Promise')]; + if (nativePromise) { + return originalFunctionToString.apply(nativePromise, arguments); + } + } + if (this === Error) { + const nativeError = g[zoneSymbol('Error')]; + if (nativeError) { + return originalFunctionToString.apply(nativeError, arguments); + } + } + } + return originalFunctionToString.apply(this, arguments); + }; +} diff --git a/lib/common/utils.ts b/lib/common/utils.ts index 2a82ad7e5..dc1c3e91a 100644 --- a/lib/common/utils.ts +++ b/lib/common/utils.ts @@ -34,9 +34,11 @@ export function patchPrototype(prototype: any, fnNames: string[]) { const delegate = prototype[name]; if (delegate) { prototype[name] = ((delegate: Function) => { - return function() { + const patched: any = function() { return delegate.apply(this, bindArguments(arguments, source + '.' + name)); }; + attachOriginToPatched(patched, delegate); + return patched; })(delegate); } } @@ -401,6 +403,8 @@ const originalInstanceKey = zoneSymbol('originalInstance'); export function patchClass(className: string) { const OriginalClass = _global[className]; if (!OriginalClass) return; + // keep original class in global + _global[zoneSymbol(className)] = OriginalClass; _global[className] = function() { const a = bindArguments(arguments, className); @@ -425,6 +429,9 @@ export function patchClass(className: string) { } }; + // attach original delegate to patched function + attachOriginToPatched(_global[className], OriginalClass); + const instance = new OriginalClass(function() {}); let prop; @@ -441,6 +448,10 @@ export function patchClass(className: string) { set: function(fn) { if (typeof fn === 'function') { this[originalInstanceKey][prop] = Zone.current.wrap(fn, className + '.' + prop); + // keep callback in wrapped function so we can + // use it in Function.prototype.toString to return + // the native one. + attachOriginToPatched(this[originalInstanceKey][prop], fn); } else { this[originalInstanceKey][prop] = fn; } @@ -488,6 +499,7 @@ export function patchMethod( if (proto && !(delegate = proto[delegateName])) { delegate = proto[delegateName] = proto[name]; proto[name] = createNamedFn(name, patchFn(delegate, delegateName, name)); + attachOriginToPatched(proto[name], delegate); } return delegate; } @@ -575,5 +587,9 @@ export function findEventTask(target: any, evtName: string): Task[] { return result; } +export function attachOriginToPatched(patched: Function, original: any) { + (patched as any)[zoneSymbol('OriginalDelegate')] = original; +} + (Zone as any)[zoneSymbol('patchEventTargetMethods')] = patchEventTargetMethods; (Zone as any)[zoneSymbol('patchOnProperties')] = patchOnProperties; diff --git a/lib/node/node.ts b/lib/node/node.ts index 85443a28c..42ed632ee 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -11,6 +11,7 @@ import './events'; import './fs'; import {patchTimer} from '../common/timers'; +import {patchFuncToString} from '../common/to-string'; import {findEventTask, patchMacroTask, patchMicroTask, zoneSymbol} from '../common/utils'; const set = 'set'; @@ -35,6 +36,9 @@ if (shouldPatchGlobalTimers) { patchProcess(); handleUnhandledPromiseRejection(); +// patch Function.prototyp.toString +patchFuncToString(); + // Crypto let crypto: any; try { diff --git a/test/common/toString.spec.ts b/test/common/toString.spec.ts new file mode 100644 index 000000000..8e735835a --- /dev/null +++ b/test/common/toString.spec.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports} from '../test-util'; + +const zoneSymbolString = '__zone_symbol__'; +const g: any = + typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global; +describe('global function patch', () => { + describe('isOriginal', () => { + it('setTimeout toString should be the same with non patched setTimeout', () => { + expect(Function.prototype.toString.call(setTimeout)) + .toEqual(Function.prototype.toString.call(g[zoneSymbol('setTimeout')])); + }); + }); + + describe('isNative', () => { + it('ZoneAwareError toString should look like native', () => { + expect(Function.prototype.toString.call(Error)).toContain('[native code]'); + }); + + it('EventTarget addEventListener should look like native', ifEnvSupports('HTMLElement', () => { + expect(Function.prototype.toString.call(HTMLElement.prototype.addEventListener)) + .toContain('[native code]'); + })); + }); +}); diff --git a/test/common_tests.ts b/test/common_tests.ts index 67716e2b1..bd684d273 100644 --- a/test/common_tests.ts +++ b/test/common_tests.ts @@ -14,6 +14,7 @@ import './common/Promise.spec'; import './common/Error.spec'; import './common/setInterval.spec'; import './common/setTimeout.spec'; +import './common/toString.spec'; import './zone-spec/long-stack-trace-zone.spec'; import './zone-spec/async-test.spec'; import './zone-spec/sync-test.spec';