/*! * depd * Copyright(c) 2014 Douglas Christopher Wilson * MIT Licensed */ /** * Module dependencies. */ var callSiteToString = require('./lib/compat').callSiteToString var EventEmitter = require('events').EventEmitter var relative = require('path').relative /** * Module exports. */ module.exports = depd /** * Get the path to base files on. */ var basePath = process.cwd() /** * Get listener count on event emitter. */ /*istanbul ignore next*/ var eventListenerCount = EventEmitter.listenerCount || function (emitter, type) { return emitter.listeners(type).length } /** * Determine if namespace is contained in the string. */ function containsNamespace(str, namespace) { var val = str.split(/[ ,]+/) namespace = String(namespace).toLowerCase() for (var i = 0 ; i < val.length; i++) { if (!(str = val[i])) continue; // namespace contained if (str === '*' || str.toLowerCase() === namespace) { return true } } return false } /** * Convert a data descriptor to accessor descriptor. */ function convertDataDescriptorToAccessor(obj, prop, message) { var descriptor = Object.getOwnPropertyDescriptor(obj, prop) var value = descriptor.value descriptor.get = function getter() { return value } if (descriptor.writable) { descriptor.set = function setter(val) { return value = val } } delete descriptor.value delete descriptor.writable Object.defineProperty(obj, prop, descriptor) return descriptor } /** * Create arguments string to keep arity. */ function createArgumentsString(arity) { var str = '' for (var i = 0; i < arity; i++) { str += ', arg' + i } return str.substr(2) } /** * Create stack string from stack. */ function createStackString(stack) { var str = this.name + ': ' + this.namespace if (this.message) { str += ' deprecated ' + this.message } for (var i = 0; i < stack.length; i++) { str += '\n at ' + callSiteToString(stack[i]) } return str } /** * Create deprecate for namespace in caller. */ function depd(namespace) { if (!namespace) { throw new TypeError('argument namespace is required') } var stack = getStack() var site = callSiteLocation(stack[1]) var file = site[0] function deprecate(message) { // call to self as log log.call(deprecate, message) } deprecate._file = file deprecate._ignored = isignored(namespace) deprecate._namespace = namespace deprecate._traced = istraced(namespace) deprecate._warned = Object.create(null) deprecate.function = wrapfunction deprecate.property = wrapproperty return deprecate } /** * Determine if namespace is ignored. */ function isignored(namespace) { /* istanbul ignore next: tested in a child processs */ if (process.noDeprecation) { // --no-deprecation support return true } var str = process.env.NO_DEPRECATION || '' // namespace ignored return containsNamespace(str, namespace) } /** * Determine if namespace is traced. */ function istraced(namespace) { /* istanbul ignore next: tested in a child processs */ if (process.traceDeprecation) { // --trace-deprecation support return true } var str = process.env.TRACE_DEPRECATION || '' // namespace traced return containsNamespace(str, namespace) } /** * Display deprecation message. */ function log(message, site) { var haslisteners = eventListenerCount(process, 'deprecation') !== 0 // abort early if no destination if (!haslisteners && this._ignored) { return } var caller var callFile var callSite var i = 0 var seen = false var stack = getStack() var file = this._file if (site) { // provided site callSite = callSiteLocation(stack[1]) callSite.name = site.name file = callSite[0] } else { // get call site i = 2 site = callSiteLocation(stack[i]) callSite = site } // get caller of deprecated thing in relation to file for (; i < stack.length; i++) { caller = callSiteLocation(stack[i]) callFile = caller[0] if (callFile === file) { seen = true } else if (callFile === this._file) { file = this._file } else if (seen) { break } } var key = caller ? site.join(':') + '__' + caller.join(':') : undefined if (key !== undefined && key in this._warned) { // already warned return } this._warned[key] = true // generate automatic message from call site if (!message) { message = callSite === site || !callSite.name ? defaultMessage(site) : defaultMessage(callSite) } // emit deprecation if listeners exist if (haslisteners) { var err = DeprecationError(this._namespace, message, stack.slice(i)) process.emit('deprecation', err) return } // format and write message var format = process.stderr.isTTY ? formatColor : formatPlain var msg = format.call(this, message, caller, stack.slice(i)) process.stderr.write(msg + '\n', 'utf8') return } /** * Get call site location as array. */ function callSiteLocation(callSite) { var file = callSite.getFileName() || '' var line = callSite.getLineNumber() var colm = callSite.getColumnNumber() if (callSite.isEval()) { file = callSite.getEvalOrigin() + ', ' + file } var site = [file, line, colm] site.callSite = callSite site.name = callSite.getFunctionName() return site } /** * Generate a default message from the site. */ function defaultMessage(site) { var callSite = site.callSite var funcName = site.name var typeName = callSite.getTypeName() // make useful anonymous name if (!funcName) { funcName = '' } // make useful type name if (typeName === 'Function') { typeName = callSite.getThis().name || typeName } return callSite.getMethodName() ? typeName + '.' + funcName : funcName } /** * Format deprecation message without color. */ function formatPlain(msg, caller, stack) { var timestamp = new Date().toUTCString() var formatted = timestamp + ' ' + this._namespace + ' deprecated ' + msg // add stack trace if (this._traced) { for (var i = 0; i < stack.length; i++) { formatted += '\n at ' + callSiteToString(stack[i]) } return formatted } if (caller) { formatted += ' at ' + formatLocation(caller) } return formatted } /** * Format deprecation message with color. */ function formatColor(msg, caller, stack) { var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' // bold cyan + ' \x1b[33;1mdeprecated\x1b[22;39m' // bold yellow + ' \x1b[0m' + msg + '\x1b[39m' // reset // add stack trace if (this._traced) { for (var i = 0; i < stack.length; i++) { formatted += '\n \x1b[36mat ' + callSiteToString(stack[i]) + '\x1b[39m' // cyan } return formatted } if (caller) { formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan } return formatted } /** * Format call site location. */ function formatLocation(callSite) { return relative(basePath, callSite[0]) + ':' + callSite[1] + ':' + callSite[2] } /** * Get the stack as array of call sites. */ function getStack() { var limit = Error.stackTraceLimit var obj = {} var prep = Error.prepareStackTrace Error.prepareStackTrace = prepareObjectStackTrace Error.stackTraceLimit = Math.max(10, limit) // capture the stack Error.captureStackTrace(obj) // slice this function off the top var stack = obj.stack.slice(1) Error.prepareStackTrace = prep Error.stackTraceLimit = limit return stack } /** * Capture call site stack from v8. */ function prepareObjectStackTrace(obj, stack) { return stack } /** * Return a wrapped function in a deprecation message. */ function wrapfunction(fn, message) { if (typeof fn !== 'function') { throw new TypeError('argument fn must be a function') } var args = createArgumentsString(fn.length) var deprecate = this var stack = getStack() var site = callSiteLocation(stack[1]) site.name = fn.name var deprecatedfn = eval('(function (' + args + ') {\n' + '"use strict"\n' + 'log.call(deprecate, message, site)\n' + 'return fn.apply(this, arguments)\n' + '})') return deprecatedfn } /** * Wrap property in a deprecation message. */ function wrapproperty(obj, prop, message) { if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { throw new TypeError('argument obj must be object') } var descriptor = Object.getOwnPropertyDescriptor(obj, prop) if (!descriptor) { throw new TypeError('must call property on owner object') } if (!descriptor.configurable) { throw new TypeError('property must be configurable') } var deprecate = this var stack = getStack() var site = callSiteLocation(stack[1]) // set site name site.name = prop // convert data descriptor if ('value' in descriptor) { descriptor = convertDataDescriptorToAccessor(obj, prop, message) } var get = descriptor.get var set = descriptor.set // wrap getter if (typeof get === 'function') { descriptor.get = function getter() { log.call(deprecate, message, site) return get.apply(this, arguments) } } // wrap setter if (typeof set === 'function') { descriptor.set = function setter() { log.call(deprecate, message, site) return set.apply(this, arguments) } } Object.defineProperty(obj, prop, descriptor) } /** * Create DeprecationError for deprecation */ function DeprecationError(namespace, message, stack) { var error = new Error() var stackString Object.defineProperty(error, 'constructor', { value: DeprecationError }) Object.defineProperty(error, 'message', { configurable: true, enumerable: false, value: message, writable: true }) Object.defineProperty(error, 'name', { enumerable: false, configurable: true, value: 'DeprecationError', writable: true }) Object.defineProperty(error, 'namespace', { configurable: true, enumerable: false, value: namespace, writable: true }) Object.defineProperty(error, 'stack', { configurable: true, enumerable: false, get: function () { if (stackString !== undefined) { return stackString } // prepare stack trace return stackString = createStackString.call(this, stack) }, set: function setter(val) { stackString = val } }) return error }