You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

529 lines
10 KiB

8 years ago
  1. /*!
  2. * depd
  3. * Copyright(c) 2014 Douglas Christopher Wilson
  4. * MIT Licensed
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var callSiteToString = require('./lib/compat').callSiteToString
  10. var EventEmitter = require('events').EventEmitter
  11. var relative = require('path').relative
  12. /**
  13. * Module exports.
  14. */
  15. module.exports = depd
  16. /**
  17. * Get the path to base files on.
  18. */
  19. var basePath = process.cwd()
  20. /**
  21. * Get listener count on event emitter.
  22. */
  23. /*istanbul ignore next*/
  24. var eventListenerCount = EventEmitter.listenerCount
  25. || function (emitter, type) { return emitter.listeners(type).length }
  26. /**
  27. * Determine if namespace is contained in the string.
  28. */
  29. function containsNamespace(str, namespace) {
  30. var val = str.split(/[ ,]+/)
  31. namespace = String(namespace).toLowerCase()
  32. for (var i = 0 ; i < val.length; i++) {
  33. if (!(str = val[i])) continue;
  34. // namespace contained
  35. if (str === '*' || str.toLowerCase() === namespace) {
  36. return true
  37. }
  38. }
  39. return false
  40. }
  41. /**
  42. * Convert a data descriptor to accessor descriptor.
  43. */
  44. function convertDataDescriptorToAccessor(obj, prop, message) {
  45. var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
  46. var value = descriptor.value
  47. descriptor.get = function getter() { return value }
  48. if (descriptor.writable) {
  49. descriptor.set = function setter(val) { return value = val }
  50. }
  51. delete descriptor.value
  52. delete descriptor.writable
  53. Object.defineProperty(obj, prop, descriptor)
  54. return descriptor
  55. }
  56. /**
  57. * Create arguments string to keep arity.
  58. */
  59. function createArgumentsString(arity) {
  60. var str = ''
  61. for (var i = 0; i < arity; i++) {
  62. str += ', arg' + i
  63. }
  64. return str.substr(2)
  65. }
  66. /**
  67. * Create stack string from stack.
  68. */
  69. function createStackString(stack) {
  70. var str = this.name + ': ' + this.namespace
  71. if (this.message) {
  72. str += ' deprecated ' + this.message
  73. }
  74. for (var i = 0; i < stack.length; i++) {
  75. str += '\n at ' + callSiteToString(stack[i])
  76. }
  77. return str
  78. }
  79. /**
  80. * Create deprecate for namespace in caller.
  81. */
  82. function depd(namespace) {
  83. if (!namespace) {
  84. throw new TypeError('argument namespace is required')
  85. }
  86. var stack = getStack()
  87. var site = callSiteLocation(stack[1])
  88. var file = site[0]
  89. function deprecate(message) {
  90. // call to self as log
  91. log.call(deprecate, message)
  92. }
  93. deprecate._file = file
  94. deprecate._ignored = isignored(namespace)
  95. deprecate._namespace = namespace
  96. deprecate._traced = istraced(namespace)
  97. deprecate._warned = Object.create(null)
  98. deprecate.function = wrapfunction
  99. deprecate.property = wrapproperty
  100. return deprecate
  101. }
  102. /**
  103. * Determine if namespace is ignored.
  104. */
  105. function isignored(namespace) {
  106. /* istanbul ignore next: tested in a child processs */
  107. if (process.noDeprecation) {
  108. // --no-deprecation support
  109. return true
  110. }
  111. var str = process.env.NO_DEPRECATION || ''
  112. // namespace ignored
  113. return containsNamespace(str, namespace)
  114. }
  115. /**
  116. * Determine if namespace is traced.
  117. */
  118. function istraced(namespace) {
  119. /* istanbul ignore next: tested in a child processs */
  120. if (process.traceDeprecation) {
  121. // --trace-deprecation support
  122. return true
  123. }
  124. var str = process.env.TRACE_DEPRECATION || ''
  125. // namespace traced
  126. return containsNamespace(str, namespace)
  127. }
  128. /**
  129. * Display deprecation message.
  130. */
  131. function log(message, site) {
  132. var haslisteners = eventListenerCount(process, 'deprecation') !== 0
  133. // abort early if no destination
  134. if (!haslisteners && this._ignored) {
  135. return
  136. }
  137. var caller
  138. var callFile
  139. var callSite
  140. var i = 0
  141. var seen = false
  142. var stack = getStack()
  143. var file = this._file
  144. if (site) {
  145. // provided site
  146. callSite = callSiteLocation(stack[1])
  147. callSite.name = site.name
  148. file = callSite[0]
  149. } else {
  150. // get call site
  151. i = 2
  152. site = callSiteLocation(stack[i])
  153. callSite = site
  154. }
  155. // get caller of deprecated thing in relation to file
  156. for (; i < stack.length; i++) {
  157. caller = callSiteLocation(stack[i])
  158. callFile = caller[0]
  159. if (callFile === file) {
  160. seen = true
  161. } else if (callFile === this._file) {
  162. file = this._file
  163. } else if (seen) {
  164. break
  165. }
  166. }
  167. var key = caller
  168. ? site.join(':') + '__' + caller.join(':')
  169. : undefined
  170. if (key !== undefined && key in this._warned) {
  171. // already warned
  172. return
  173. }
  174. this._warned[key] = true
  175. // generate automatic message from call site
  176. if (!message) {
  177. message = callSite === site || !callSite.name
  178. ? defaultMessage(site)
  179. : defaultMessage(callSite)
  180. }
  181. // emit deprecation if listeners exist
  182. if (haslisteners) {
  183. var err = DeprecationError(this._namespace, message, stack.slice(i))
  184. process.emit('deprecation', err)
  185. return
  186. }
  187. // format and write message
  188. var format = process.stderr.isTTY
  189. ? formatColor
  190. : formatPlain
  191. var msg = format.call(this, message, caller, stack.slice(i))
  192. process.stderr.write(msg + '\n', 'utf8')
  193. return
  194. }
  195. /**
  196. * Get call site location as array.
  197. */
  198. function callSiteLocation(callSite) {
  199. var file = callSite.getFileName() || '<anonymous>'
  200. var line = callSite.getLineNumber()
  201. var colm = callSite.getColumnNumber()
  202. if (callSite.isEval()) {
  203. file = callSite.getEvalOrigin() + ', ' + file
  204. }
  205. var site = [file, line, colm]
  206. site.callSite = callSite
  207. site.name = callSite.getFunctionName()
  208. return site
  209. }
  210. /**
  211. * Generate a default message from the site.
  212. */
  213. function defaultMessage(site) {
  214. var callSite = site.callSite
  215. var funcName = site.name
  216. // make useful anonymous name
  217. if (!funcName) {
  218. funcName = '<anonymous@' + formatLocation(site) + '>'
  219. }
  220. var context = callSite.getThis()
  221. var typeName = context && callSite.getTypeName()
  222. // ignore useless type name
  223. if (typeName === 'Object') {
  224. typeName = undefined
  225. }
  226. // make useful type name
  227. if (typeName === 'Function') {
  228. typeName = context.name || typeName
  229. }
  230. return typeName && callSite.getMethodName()
  231. ? typeName + '.' + funcName
  232. : funcName
  233. }
  234. /**
  235. * Format deprecation message without color.
  236. */
  237. function formatPlain(msg, caller, stack) {
  238. var timestamp = new Date().toUTCString()
  239. var formatted = timestamp
  240. + ' ' + this._namespace
  241. + ' deprecated ' + msg
  242. // add stack trace
  243. if (this._traced) {
  244. for (var i = 0; i < stack.length; i++) {
  245. formatted += '\n at ' + callSiteToString(stack[i])
  246. }
  247. return formatted
  248. }
  249. if (caller) {
  250. formatted += ' at ' + formatLocation(caller)
  251. }
  252. return formatted
  253. }
  254. /**
  255. * Format deprecation message with color.
  256. */
  257. function formatColor(msg, caller, stack) {
  258. var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' // bold cyan
  259. + ' \x1b[33;1mdeprecated\x1b[22;39m' // bold yellow
  260. + ' \x1b[0m' + msg + '\x1b[39m' // reset
  261. // add stack trace
  262. if (this._traced) {
  263. for (var i = 0; i < stack.length; i++) {
  264. formatted += '\n \x1b[36mat ' + callSiteToString(stack[i]) + '\x1b[39m' // cyan
  265. }
  266. return formatted
  267. }
  268. if (caller) {
  269. formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan
  270. }
  271. return formatted
  272. }
  273. /**
  274. * Format call site location.
  275. */
  276. function formatLocation(callSite) {
  277. return relative(basePath, callSite[0])
  278. + ':' + callSite[1]
  279. + ':' + callSite[2]
  280. }
  281. /**
  282. * Get the stack as array of call sites.
  283. */
  284. function getStack() {
  285. var limit = Error.stackTraceLimit
  286. var obj = {}
  287. var prep = Error.prepareStackTrace
  288. Error.prepareStackTrace = prepareObjectStackTrace
  289. Error.stackTraceLimit = Math.max(10, limit)
  290. // capture the stack
  291. Error.captureStackTrace(obj)
  292. // slice this function off the top
  293. var stack = obj.stack.slice(1)
  294. Error.prepareStackTrace = prep
  295. Error.stackTraceLimit = limit
  296. return stack
  297. }
  298. /**
  299. * Capture call site stack from v8.
  300. */
  301. function prepareObjectStackTrace(obj, stack) {
  302. return stack
  303. }
  304. /**
  305. * Return a wrapped function in a deprecation message.
  306. */
  307. function wrapfunction(fn, message) {
  308. if (typeof fn !== 'function') {
  309. throw new TypeError('argument fn must be a function')
  310. }
  311. var args = createArgumentsString(fn.length)
  312. var deprecate = this
  313. var stack = getStack()
  314. var site = callSiteLocation(stack[1])
  315. site.name = fn.name
  316. var deprecatedfn = eval('(function (' + args + ') {\n'
  317. + '"use strict"\n'
  318. + 'log.call(deprecate, message, site)\n'
  319. + 'return fn.apply(this, arguments)\n'
  320. + '})')
  321. return deprecatedfn
  322. }
  323. /**
  324. * Wrap property in a deprecation message.
  325. */
  326. function wrapproperty(obj, prop, message) {
  327. if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
  328. throw new TypeError('argument obj must be object')
  329. }
  330. var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
  331. if (!descriptor) {
  332. throw new TypeError('must call property on owner object')
  333. }
  334. if (!descriptor.configurable) {
  335. throw new TypeError('property must be configurable')
  336. }
  337. var deprecate = this
  338. var stack = getStack()
  339. var site = callSiteLocation(stack[1])
  340. // set site name
  341. site.name = prop
  342. // convert data descriptor
  343. if ('value' in descriptor) {
  344. descriptor = convertDataDescriptorToAccessor(obj, prop, message)
  345. }
  346. var get = descriptor.get
  347. var set = descriptor.set
  348. // wrap getter
  349. if (typeof get === 'function') {
  350. descriptor.get = function getter() {
  351. log.call(deprecate, message, site)
  352. return get.apply(this, arguments)
  353. }
  354. }
  355. // wrap setter
  356. if (typeof set === 'function') {
  357. descriptor.set = function setter() {
  358. log.call(deprecate, message, site)
  359. return set.apply(this, arguments)
  360. }
  361. }
  362. Object.defineProperty(obj, prop, descriptor)
  363. }
  364. /**
  365. * Create DeprecationError for deprecation
  366. */
  367. function DeprecationError(namespace, message, stack) {
  368. var error = new Error()
  369. var stackString
  370. Object.defineProperty(error, 'constructor', {
  371. value: DeprecationError
  372. })
  373. Object.defineProperty(error, 'message', {
  374. configurable: true,
  375. enumerable: false,
  376. value: message,
  377. writable: true
  378. })
  379. Object.defineProperty(error, 'name', {
  380. enumerable: false,
  381. configurable: true,
  382. value: 'DeprecationError',
  383. writable: true
  384. })
  385. Object.defineProperty(error, 'namespace', {
  386. configurable: true,
  387. enumerable: false,
  388. value: namespace,
  389. writable: true
  390. })
  391. Object.defineProperty(error, 'stack', {
  392. configurable: true,
  393. enumerable: false,
  394. get: function () {
  395. if (stackString !== undefined) {
  396. return stackString
  397. }
  398. // prepare stack trace
  399. return stackString = createStackString.call(this, stack)
  400. },
  401. set: function setter(val) {
  402. stackString = val
  403. }
  404. })
  405. return error
  406. }