From 649cb681b6e5aaa888014e781950a392c79d6fbb Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 12 Jul 2015 19:52:30 -0400 Subject: [PATCH] [FEATURE ember-debug-handlers] Implement [emberjs/rfcs#65](https://github.com/emberjs/rfcs/blob/master/text/0065-deprecation-warning-handlers.md). --- FEATURES.md | 5 + features.json | 3 +- packages/ember-debug/lib/deprecate.js | 93 +++++++++++ .../ember-debug/lib/deprecation-manager.js | 26 ---- packages/ember-debug/lib/handlers.js | 27 ++++ packages/ember-debug/lib/is-plain-function.js | 3 + packages/ember-debug/lib/main.js | 125 ++------------- packages/ember-debug/lib/warn.js | 27 ++++ packages/ember-debug/tests/handlers-test.js | 146 ++++++++++++++++++ packages/ember-debug/tests/main_test.js | 56 ++++--- packages/ember-metal/lib/main.js | 7 + tests/index.html | 8 - 12 files changed, 359 insertions(+), 167 deletions(-) create mode 100644 packages/ember-debug/lib/deprecate.js delete mode 100644 packages/ember-debug/lib/deprecation-manager.js create mode 100644 packages/ember-debug/lib/handlers.js create mode 100644 packages/ember-debug/lib/is-plain-function.js create mode 100644 packages/ember-debug/lib/warn.js create mode 100644 packages/ember-debug/tests/handlers-test.js diff --git a/FEATURES.md b/FEATURES.md index 933ce3d4208..50e7e20c91c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -320,3 +320,8 @@ for a detailed explanation. Implements RFC https://github.com/emberjs/rfcs/pull/58, adding support for dashless helpers. + +* `ember-debug-handlers` + + Implemencts RFC https://github.com/emberjs/rfcs/pull/65, adding support for + custom deprecation and warning handlers. diff --git a/features.json b/features.json index 2be84ebca86..d937f4e9829 100644 --- a/features.json +++ b/features.json @@ -21,7 +21,8 @@ "ember-routing-htmlbars-improved-actions": true, "ember-htmlbars-get-helper": true, "ember-htmlbars-helper": true, - "ember-htmlbars-dashless-helpers": true + "ember-htmlbars-dashless-helpers": true, + "ember-debug-handlers": null }, "debugStatements": [ "Ember.warn", diff --git a/packages/ember-debug/lib/deprecate.js b/packages/ember-debug/lib/deprecate.js new file mode 100644 index 00000000000..fd72e52850f --- /dev/null +++ b/packages/ember-debug/lib/deprecate.js @@ -0,0 +1,93 @@ +/*global __fail__*/ + +import Ember from 'ember-metal/core'; +import EmberError from 'ember-metal/error'; +import Logger from 'ember-metal/logger'; +import { registerHandler as genericRegisterHandler, invoke } from 'ember-debug/handlers'; + +export function registerHandler(handler) { + genericRegisterHandler('deprecate', handler); +} + +function formatMessage(_message, options) { + let message = _message; + + if (options && options.id) { + message = message + ` [deprecation id: ${options.id}]`; + } + + if (options && options.url) { + message += ' See ' + options.url + ' for more details.'; + } + + return message; +} + +registerHandler(function logDeprecationToConsole(message, options) { + let updatedMessage = formatMessage(message, options); + + Logger.warn('DEPRECATION: ' + updatedMessage); +}); + +registerHandler(function logDeprecationStackTrace(message, options, next) { + if (Ember.LOG_STACKTRACE_ON_DEPRECATION) { + let stackStr = ''; + let error, stack; + + // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome + try { __fail__.fail(); } catch (e) { error = e; } + + if (error.stack) { + if (error['arguments']) { + // Chrome + stack = error.stack.replace(/^\s+at\s+/gm, ''). + replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2'). + replace(/^Object.\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); + stack.shift(); + } else { + // Firefox + stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). + replace(/^\(/gm, '{anonymous}(').split('\n'); + } + + stackStr = '\n ' + stack.slice(2).join('\n '); + } + + let updatedMessage = formatMessage(message, options); + + Logger.warn('DEPRECATION: ' + updatedMessage + stackStr); + } else { + next(...arguments); + } +}); + +registerHandler(function raiseOnDeprecation(message, options, next) { + if (Ember.ENV.RAISE_ON_DEPRECATION) { + let updatedMessage = formatMessage(message); + + throw new EmberError(updatedMessage); + } else { + next(...arguments); + } +}); + +/** + Display a deprecation warning with the provided message and a stack trace + (Chrome and Firefox only). Ember build tools will remove any calls to + `Ember.deprecate()` when doing a production build. + + @method deprecate + @param {String} message A description of the deprecation. + @param {Boolean|Function} test An optional boolean. If falsy, the deprecation + will be displayed. If this is a function, it will be executed and its return + value will be used as condition. + @param {Object} options An optional object that can be used to pass + in a `url` to the transition guide on the emberjs.com website, and a unique + `id` for this deprecation. The `id` can be used by Ember debugging tools + to change the behavior (raise, log or silence) for that specific deprecation. + The `id` should be namespaced by dots, e.g. "view.helper.select". + @public +*/ +export default function() { + invoke('deprecate', ...arguments); +} diff --git a/packages/ember-debug/lib/deprecation-manager.js b/packages/ember-debug/lib/deprecation-manager.js deleted file mode 100644 index 92aef4adfca..00000000000 --- a/packages/ember-debug/lib/deprecation-manager.js +++ /dev/null @@ -1,26 +0,0 @@ -import dictionary from 'ember-metal/dictionary'; -import { symbol } from 'ember-metal/utils'; - -export const deprecationLevels = { - RAISE: symbol('RAISE'), - LOG: symbol('LOG'), - SILENCE: symbol('SILENCE') -}; - -export default { - defaultLevel: deprecationLevels.LOG, - individualLevels: dictionary(null), - setDefaultLevel(level) { - this.defaultLevel = level; - }, - setLevel(id, level) { - this.individualLevels[id] = level; - }, - getLevel(id) { - let level = this.individualLevels[id]; - if (!level) { - level = this.defaultLevel; - } - return level; - } -}; diff --git a/packages/ember-debug/lib/handlers.js b/packages/ember-debug/lib/handlers.js new file mode 100644 index 00000000000..cc889e384d4 --- /dev/null +++ b/packages/ember-debug/lib/handlers.js @@ -0,0 +1,27 @@ +import isPlainFunction from 'ember-debug/is-plain-function'; + +export let HANDLERS = { }; + +function normalizeTest(test) { + return isPlainFunction(test) ? test() : test; +} + +export function registerHandler(type, callback) { + let nextHandler = HANDLERS[type] || function() { }; + + HANDLERS[type] = function(message, options) { + callback(message, options, nextHandler); + }; +} + +export function invoke(type, message, test, options) { + if (normalizeTest(test)) { return; } + + let handlerForType = HANDLERS[type]; + + if (!handlerForType) { return; } + + if (handlerForType) { + handlerForType(message, options); + } +} diff --git a/packages/ember-debug/lib/is-plain-function.js b/packages/ember-debug/lib/is-plain-function.js new file mode 100644 index 00000000000..925741604e3 --- /dev/null +++ b/packages/ember-debug/lib/is-plain-function.js @@ -0,0 +1,3 @@ +export default function isPlainFunction(test) { + return typeof test === 'function' && test.PrototypeMixin === undefined; +} diff --git a/packages/ember-debug/lib/main.js b/packages/ember-debug/lib/main.js index 188a5a6cbfd..719569f7154 100644 --- a/packages/ember-debug/lib/main.js +++ b/packages/ember-debug/lib/main.js @@ -5,9 +5,16 @@ import { registerDebugFunction } from 'ember-metal/assert'; import isEnabled, { FEATURES } from 'ember-metal/features'; import EmberError from 'ember-metal/error'; import Logger from 'ember-metal/logger'; -import deprecationManager, { deprecationLevels } from 'ember-debug/deprecation-manager'; - import environment from 'ember-metal/environment'; +import deprecate, { + registerHandler as registerDeprecationHandler +} from 'ember-debug/deprecate'; +import warn, { + registerHandler as registerWarnHandler +} from 'ember-debug/warn'; +import isPlainFunction from 'ember-debug/is-plain-function'; + +Ember.deprecate = deprecate; /** @module ember @@ -19,9 +26,6 @@ import environment from 'ember-metal/environment'; @public */ -function isPlainFunction(test) { - return typeof test === 'function' && test.PrototypeMixin === undefined; -} /** Define an assertion that will throw an exception if the condition is not @@ -58,26 +62,6 @@ function assert(desc, test) { } } - -/** - Display a warning with the provided message. Ember build tools will - remove any calls to `Ember.warn()` when doing a production build. - - @method warn - @param {String} message A warning to display. - @param {Boolean} test An optional boolean. If falsy, the warning - will be displayed. - @public -*/ -function warn(message, test) { - if (!test) { - Logger.warn('WARNING: ' + message); - if ('trace' in Logger) { - Logger.trace(); - } - } -} - /** Display a debug notice. Ember build tools will remove any calls to `Ember.debug()` when doing a production build. @@ -94,86 +78,6 @@ function debug(message) { Logger.debug('DEBUG: ' + message); } -/** - Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only). Ember build tools will remove any calls to - `Ember.deprecate()` when doing a production build. - - @method deprecate - @param {String} message A description of the deprecation. - @param {Boolean|Function} test An optional boolean. If falsy, the deprecation - will be displayed. If this is a function, it will be executed and its return - value will be used as condition. - @param {Object} options An optional object that can be used to pass - in a `url` to the transition guide on the emberjs.com website, and a unique - `id` for this deprecation. The `id` can be used by Ember debugging tools - to change the behavior (raise, log or silence) for that specific deprecation. - The `id` should be namespaced by dots, e.g. "view.helper.select". - @public -*/ -function deprecate(message, test, options) { - if (Ember.ENV.RAISE_ON_DEPRECATION) { - deprecationManager.setDefaultLevel(deprecationLevels.RAISE); - } - if (deprecationManager.getLevel(options && options.id) === deprecationLevels.SILENCE) { - return; - } - - var noDeprecation; - - if (isPlainFunction(test)) { - noDeprecation = test(); - } else { - noDeprecation = test; - } - - if (noDeprecation) { return; } - - if (options && options.id) { - message = message + ` [deprecation id: ${options.id}]`; - } - - if (deprecationManager.getLevel(options && options.id) === deprecationLevels.RAISE) { - throw new EmberError(message); - } - - var error; - - // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome - try { __fail__.fail(); } catch (e) { error = e; } - - if (arguments.length === 3) { - Ember.assert('options argument to Ember.deprecate should be an object', options && typeof options === 'object'); - if (options.url) { - message += ' See ' + options.url + ' for more details.'; - } - } - - if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) { - var stack; - var stackStr = ''; - - if (error['arguments']) { - // Chrome - stack = error.stack.replace(/^\s+at\s+/gm, ''). - replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2'). - replace(/^Object.\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); - stack.shift(); - } else { - // Firefox - stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). - replace(/^\(/gm, '{anonymous}(').split('\n'); - } - - stackStr = '\n ' + stack.slice(2).join('\n '); - message = message + stackStr; - } - - Logger.warn('DEPRECATION: ' + message); -} - - - /** Alias an old, deprecated method with its new counterpart. @@ -297,13 +201,12 @@ if (!Ember.testing) { } } -Ember.Debug = { - _addDeprecationLevel(id, level) { - deprecationManager.setLevel(id, level); - }, - _deprecationLevels: deprecationLevels -}; +Ember.Debug = { }; +if (isEnabled('ember-debug-handlers')) { + Ember.Debug.registerDeprecationHandler = registerDeprecationHandler; + Ember.Debug.registerWarnHandler = registerWarnHandler; +} /* We are transitioning away from `ember.js` to `ember.debug.js` to make it much clearer that it is only for local development purposes. diff --git a/packages/ember-debug/lib/warn.js b/packages/ember-debug/lib/warn.js new file mode 100644 index 00000000000..bf8e7366365 --- /dev/null +++ b/packages/ember-debug/lib/warn.js @@ -0,0 +1,27 @@ +import Logger from 'ember-metal/logger'; +import { registerHandler as genericRegisterHandler, invoke } from 'ember-debug/handlers'; + +export function registerHandler(handler) { + genericRegisterHandler('warn', handler); +} + +registerHandler(function logWarning(message, options) { + Logger.warn('WARNING: ' + message); + if ('trace' in Logger) { + Logger.trace(); + } +}); + +/** + Display a warning with the provided message. Ember build tools will + remove any calls to `Ember.warn()` when doing a production build. + + @method warn + @param {String} message A warning to display. + @param {Boolean} test An optional boolean. If falsy, the warning + will be displayed. + @public +*/ +export default function warn() { + invoke('warn', ...arguments); +} diff --git a/packages/ember-debug/tests/handlers-test.js b/packages/ember-debug/tests/handlers-test.js new file mode 100644 index 00000000000..a1fcbd507b2 --- /dev/null +++ b/packages/ember-debug/tests/handlers-test.js @@ -0,0 +1,146 @@ +import { + HANDLERS, + registerHandler, + invoke +} from 'ember-debug/handlers'; + +QUnit.module('ember-debug/handlers', { + teardown() { + delete HANDLERS.blarz; + } +}); + +QUnit.test('calls handler on `invoke` when `falsey`', function(assert) { + assert.expect(2); + + function handler(message) { + assert.ok(true, 'called handler'); + assert.equal(message, 'Foo bar'); + } + + registerHandler('blarz', handler); + + invoke('blarz', 'Foo bar', false); +}); + +QUnit.test('does not call handler on `invoke` when `truthy`', function(assert) { + assert.expect(0); + + function handler() { + assert.ok(false, 'called handler'); + } + + registerHandler('blarz', handler); + + invoke('blarz', 'Foo bar', true); +}); + +QUnit.test('calling `invoke` without handlers does not throw an error', function(assert) { + assert.expect(0); + + invoke('blarz', 'Foo bar', false); +}); + +QUnit.test('invoking `next` argument calls the next handler', function(assert) { + assert.expect(2); + + function handler1(message, options, next) { + assert.ok(true, 'called handler1'); + } + + function handler2(message, options, next) { + assert.ok(true, 'called handler2'); + next(message, options); + } + + registerHandler('blarz', handler1); + registerHandler('blarz', handler2); + + invoke('blarz', 'Foo', false); +}); + +QUnit.test('invoking `next` when no other handlers exists does not error', function(assert) { + assert.expect(1); + + function handler(message, options, next) { + assert.ok(true, 'called handler1'); + + next(message, options); + } + + registerHandler('blarz', handler); + + invoke('blarz', 'Foo', false); +}); + +QUnit.test('handlers are called in the proper order', function(assert) { + assert.expect(11); + + let expectedMessage = 'This is the message'; + let expectedOptions = { id: 'foo-bar' }; + let expected = ['first', 'second', 'third', 'fourth', 'fifth']; + let actualCalls = []; + + function generateHandler(item) { + return function(message, options, next) { + assert.equal(message, expectedMessage, `message supplied to ${item} handler is correct`); + assert.equal(options, expectedOptions, `options supplied to ${item} handler is correct`); + + actualCalls.push(item); + + next(message, options); + }; + } + + expected.forEach(function(item) { + registerHandler('blarz', generateHandler(item)); + }); + + invoke('blarz', expectedMessage, false, expectedOptions); + + assert.deepEqual(actualCalls, expected.reverse(), 'handlers were called in proper order'); +}); + +QUnit.test('not invoking `next` prevents further handlers from being called', function(assert) { + assert.expect(1); + + function handler1(message, options, next) { + assert.ok(false, 'called handler1'); + } + + function handler2(message, options, next) { + assert.ok(true, 'called handler2'); + } + + registerHandler('blarz', handler1); + registerHandler('blarz', handler2); + + invoke('blarz', 'Foo', false); +}); + +QUnit.test('handlers can call `next` with custom message and/or options', function(assert) { + assert.expect(4); + + let initialMessage = 'initial message'; + let initialOptions = { id: 'initial-options' }; + + let handler2Message = 'Handler2 Message'; + let handler2Options = { id: 'handler-2' }; + + function handler1(message, options, next) { + assert.equal(message, handler2Message, 'handler2 message provided to handler1'); + assert.equal(options, handler2Options, 'handler2 options provided to handler1'); + } + + function handler2(message, options, next) { + assert.equal(message, initialMessage, 'initial message provided to handler2'); + assert.equal(options, initialOptions, 'initial options proivided to handler2'); + + next(handler2Message, handler2Options); + } + + registerHandler('blarz', handler1); + registerHandler('blarz', handler2); + + invoke('blarz', initialMessage, false, initialOptions); +}); diff --git a/packages/ember-debug/tests/main_test.js b/packages/ember-debug/tests/main_test.js index dea4baca8ee..92f9c32673c 100644 --- a/packages/ember-debug/tests/main_test.js +++ b/packages/ember-debug/tests/main_test.js @@ -1,30 +1,29 @@ import Ember from 'ember-metal/core'; -import deprecationManager, { deprecationLevels } from 'ember-debug/deprecation-manager'; +import { HANDLERS } from 'ember-debug/handlers'; +import { registerHandler } from 'ember-debug/deprecate'; let originalEnvValue; -let originalDeprecationDefault; -let originalDeprecationLevels; +let originalDeprecateHandler; QUnit.module('ember-debug', { setup() { - originalDeprecationDefault = deprecationManager.defaultLevel; - originalDeprecationLevels = deprecationManager.individualLevels; originalEnvValue = Ember.ENV.RAISE_ON_DEPRECATION; + originalDeprecateHandler = HANDLERS.deprecate; - Ember.ENV.RAISE_ON_DEPRECATION = false; - deprecationManager.setDefaultLevel(deprecationLevels.RAISE); + Ember.ENV.RAISE_ON_DEPRECATION = true; }, teardown() { - deprecationManager.defaultLevel = originalDeprecationDefault; - deprecationManager.individualLevels = originalDeprecationLevels; + HANDLERS.deprecate = originalDeprecateHandler; + Ember.ENV.RAISE_ON_DEPRECATION = originalEnvValue; } }); -QUnit.test('Ember.deprecate does not throw if default level is silence', function(assert) { +QUnit.test('Ember.deprecate does not throw if RAISE_ON_DEPRECATION is false', function(assert) { assert.expect(1); - deprecationManager.setDefaultLevel(deprecationLevels.SILENCE); + + Ember.ENV.RAISE_ON_DEPRECATION = false; try { Ember.deprecate('Should not throw', false); @@ -37,23 +36,31 @@ QUnit.test('Ember.deprecate does not throw if default level is silence', functio QUnit.test('Ember.deprecate re-sets deprecation level to RAISE if ENV.RAISE_ON_DEPRECATION is set', function(assert) { assert.expect(2); - deprecationManager.setDefaultLevel(deprecationLevels.SILENCE); + Ember.ENV.RAISE_ON_DEPRECATION = false; + + try { + Ember.deprecate('Should not throw', false); + assert.ok(true, 'Ember.deprecate did not throw'); + } catch(e) { + assert.ok(false, `Expected Ember.deprecate not to throw but it did: ${e.message}`); + } Ember.ENV.RAISE_ON_DEPRECATION = true; assert.throws(function() { Ember.deprecate('Should throw', false); }, /Should throw/); - - assert.equal(deprecationManager.defaultLevel, deprecationLevels.RAISE, - 'default level re-set to RAISE'); }); QUnit.test('When ENV.RAISE_ON_DEPRECATION is true, it is still possible to silence a deprecation by id', function(assert) { assert.expect(3); Ember.ENV.RAISE_ON_DEPRECATION = true; - deprecationManager.setLevel('my-deprecation', deprecationLevels.SILENCE); + registerHandler(function(message, options, next) { + if (!options || options.id !== 'my-deprecation') { + next(...arguments); + } + }); try { Ember.deprecate('should be silenced with matching id', false, { id: 'my-deprecation' }); @@ -173,9 +180,17 @@ QUnit.test('Ember.assert does not throw if second argument is an object', functi QUnit.test('Ember.deprecate does not throw a deprecation at log and silence levels', function() { expect(4); - var id = 'ABC'; + let id = 'ABC'; + let shouldThrow = false; + + registerHandler(function(message, options, next) { + if (options && options.id === id) { + if (shouldThrow) { + throw new Error(message); + } + } + }); - deprecationManager.setLevel(id, deprecationLevels.LOG); try { Ember.deprecate('Deprecation for testing purposes', false, { id }); ok(true, 'Deprecation did not throw'); @@ -183,7 +198,6 @@ QUnit.test('Ember.deprecate does not throw a deprecation at log and silence leve ok(false, 'Deprecation was thrown despite being added to blacklist'); } - deprecationManager.setLevel(id, deprecationLevels.SILENCE); try { Ember.deprecate('Deprecation for testing purposes', false, { id }); ok(true, 'Deprecation did not throw'); @@ -191,13 +205,13 @@ QUnit.test('Ember.deprecate does not throw a deprecation at log and silence leve ok(false, 'Deprecation was thrown despite being added to blacklist'); } - deprecationManager.setLevel(id, deprecationLevels.RAISE); + shouldThrow = true; throws(function() { Ember.deprecate('Deprecation is thrown', false, { id }); }); - deprecationManager.setLevel(id, null); + throws(function() { Ember.deprecate('Deprecation is thrown', false, { id }); diff --git a/packages/ember-metal/lib/main.js b/packages/ember-metal/lib/main.js index 205f29d9efc..fa9ce353ae8 100644 --- a/packages/ember-metal/lib/main.js +++ b/packages/ember-metal/lib/main.js @@ -401,6 +401,13 @@ Ember.runInDebug = runInDebug; // This needs to be called before any deprecateFunc if (Ember.__loader.registry['ember-debug']) { requireModule('ember-debug'); +} else { + Ember.Debug = { }; + + if (isEnabled('ember-debug-handlers')) { + Ember.Debug.registerDeprecationHandler = function() { }; + Ember.Debug.registerWarnHandler = function() { }; + } } Ember.create = Ember.deprecateFunc('Ember.create is deprecated in favor of Object.create', Object.create); diff --git a/tests/index.html b/tests/index.html index ca718ab752c..3529e154b6c 100644 --- a/tests/index.html +++ b/tests/index.html @@ -148,14 +148,6 @@ })(); - -