no-deprecated-api.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. /**
  2. * @fileoverview Rule to disallow deprecated API.
  3. * @author Toru Nagashima
  4. * @copyright 2016 Toru Nagashima. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. "use strict"
  8. //------------------------------------------------------------------------------
  9. // Requirements
  10. //------------------------------------------------------------------------------
  11. const deprecatedApis = require("../util/deprecated-apis")
  12. const getDocsUrl = require("../util/get-docs-url")
  13. const getValueIfString = require("../util/get-value-if-string")
  14. //------------------------------------------------------------------------------
  15. // Helpers
  16. //------------------------------------------------------------------------------
  17. const SENTINEL_TYPE = /^(?:.+?Statement|.+?Declaration|(?:Array|ArrowFunction|Assignment|Call|Class|Function|Member|New|Object)Expression|AssignmentPattern|Program|VariableDeclarator)$/
  18. const MODULE_ITEMS = getDeprecatedItems(deprecatedApis.modules, [], [])
  19. const GLOBAL_ITEMS = getDeprecatedItems(deprecatedApis.globals, [], [])
  20. /**
  21. * Gets the array of deprecated items.
  22. *
  23. * It's the paths which are separated by dots.
  24. * E.g. `buffer.Buffer`, `events.EventEmitter.listenerCount`
  25. *
  26. * @param {object} definition - The definition of deprecated APIs.
  27. * @param {string[]} result - The array of the result.
  28. * @param {string[]} stack - The array to manage the stack of paths.
  29. * @returns {string[]} `result`.
  30. */
  31. function getDeprecatedItems(definition, result, stack) {
  32. for (const key of Object.keys(definition)) {
  33. const item = definition[key]
  34. if (key === "$call") {
  35. result.push(`${stack.join(".")}()`)
  36. }
  37. else if (key === "$constructor") {
  38. result.push(`new ${stack.join(".")}()`)
  39. }
  40. else {
  41. stack.push(key)
  42. if (item.$deprecated) {
  43. result.push(stack.join("."))
  44. }
  45. else {
  46. getDeprecatedItems(item, result, stack)
  47. }
  48. stack.pop()
  49. }
  50. }
  51. return result
  52. }
  53. /**
  54. * Converts from a version number to a version text to display.
  55. *
  56. * @param {number} value - A version number to convert.
  57. * @returns {string} Covnerted text.
  58. */
  59. function toVersionText(value) {
  60. if (value <= 0.12) {
  61. return value.toFixed(2)
  62. }
  63. if (value < 1) {
  64. return value.toFixed(1)
  65. }
  66. return String(value)
  67. }
  68. /**
  69. * Makes a replacement message.
  70. *
  71. * @param {string|null} replacedBy - The text of substitute way.
  72. * @returns {string} Replacement message.
  73. */
  74. function toReplaceMessage(replacedBy) {
  75. return replacedBy ? ` Use ${replacedBy} instead.` : ""
  76. }
  77. /**
  78. * Gets the property name from a MemberExpression node or a Property node.
  79. *
  80. * @param {ASTNode} node - A node to get.
  81. * @returns {string|null} The property name of the node.
  82. */
  83. function getPropertyName(node) {
  84. switch (node.type) {
  85. case "MemberExpression":
  86. if (node.computed) {
  87. return getValueIfString(node.property)
  88. }
  89. return node.property.name
  90. case "Property":
  91. if (node.computed) {
  92. return getValueIfString(node.key)
  93. }
  94. if (node.key.type === "Literal") {
  95. return String(node.key.value)
  96. }
  97. return node.key.name
  98. // no default
  99. }
  100. /* istanbul ignore next: unreachable */
  101. return null
  102. }
  103. /**
  104. * Checks a given node is a ImportDeclaration node.
  105. *
  106. * @param {ASTNode} node - A node to check.
  107. * @returns {boolean} `true` if the node is a ImportDeclaration node.
  108. */
  109. function isImportDeclaration(node) {
  110. return node.type === "ImportDeclaration"
  111. }
  112. /**
  113. * Finds the variable object of a given Identifier node.
  114. *
  115. * @param {ASTNode} node - An Identifier node to find.
  116. * @param {escope.Scope} initialScope - A scope to start searching.
  117. * @returns {escope.Variable} Found variable object.
  118. */
  119. function findVariable(node, initialScope) {
  120. const location = node.range[0]
  121. let variable = null
  122. // Dive into the scope that the node exists.
  123. for (const childScope of initialScope.childScopes) {
  124. const range = childScope.block.range
  125. if (range[0] <= location && location < range[1]) {
  126. variable = findVariable(node, childScope)
  127. if (variable != null) {
  128. return variable
  129. }
  130. }
  131. }
  132. // Find the variable of that name in this scope or ancestor scopes.
  133. let scope = initialScope
  134. while (scope != null) {
  135. variable = scope.set.get(node.name)
  136. if (variable != null) {
  137. return variable
  138. }
  139. scope = scope.upper
  140. }
  141. return null
  142. }
  143. /**
  144. * Gets the top member expression node.
  145. *
  146. * @param {ASTNode} identifier - The node to get.
  147. * @returns {ASTNode} The top member expression node.
  148. */
  149. function getTopMemberExpression(identifier) {
  150. if (identifier.type !== "Identifier" && identifier.type !== "Literal") {
  151. return identifier
  152. }
  153. let node = identifier
  154. while (node.parent.type === "MemberExpression") {
  155. node = node.parent
  156. }
  157. return node
  158. }
  159. /**
  160. * The definition of this rule.
  161. *
  162. * @param {RuleContext} context - The rule context to check.
  163. * @returns {object} The definition of this rule.
  164. */
  165. function create(context) {
  166. const options = context.options[0] || {}
  167. const ignoredModuleItems = options.ignoreModuleItems || []
  168. const ignoredGlobalItems = options.ignoreGlobalItems || []
  169. let globalScope = null
  170. const varStack = []
  171. /**
  172. * Reports a use of a deprecated API.
  173. *
  174. * @param {ASTNode} node - A node to report.
  175. * @param {string} name - The name of a deprecated API.
  176. * @param {{since: number, replacedBy: string}} info - Information of the API.
  177. * @returns {void}
  178. */
  179. function report(node, name, info) {
  180. context.report({
  181. node,
  182. loc: getTopMemberExpression(node).loc,
  183. message: "{{name}} was deprecated since v{{version}}.{{replace}}",
  184. data: {
  185. name,
  186. version: toVersionText(info.since),
  187. replace: toReplaceMessage(info.replacedBy),
  188. },
  189. })
  190. }
  191. /**
  192. * Reports a use of a deprecated module.
  193. *
  194. * @param {ASTNode} node - A node to report.
  195. * @param {string} name - The name of a deprecated module.
  196. * @param {{since: number, replacedBy: string, global: boolean}} info - Information of the module.
  197. * @returns {void}
  198. */
  199. function reportModule(node, name, info) {
  200. if (ignoredModuleItems.indexOf(name) === -1) {
  201. report(node, `'${name}' module`, info)
  202. }
  203. }
  204. /**
  205. * Reports a use of a deprecated property.
  206. *
  207. * @param {ASTNode} node - A node to report.
  208. * @param {string[]} path - The path to a deprecated property.
  209. * @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
  210. * @returns {void}
  211. */
  212. function reportCall(node, path, info) {
  213. const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
  214. const name = `${path.join(".")}()`
  215. if (ignored.indexOf(name) === -1) {
  216. report(node, `'${name}'`, info)
  217. }
  218. }
  219. /**
  220. * Reports a use of a deprecated property.
  221. *
  222. * @param {ASTNode} node - A node to report.
  223. * @param {string[]} path - The path to a deprecated property.
  224. * @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
  225. * @returns {void}
  226. */
  227. function reportConstructor(node, path, info) {
  228. const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
  229. const name = `new ${path.join(".")}()`
  230. if (ignored.indexOf(name) === -1) {
  231. report(node, `'${name}'`, info)
  232. }
  233. }
  234. /**
  235. * Reports a use of a deprecated property.
  236. *
  237. * @param {ASTNode} node - A node to report.
  238. * @param {string[]} path - The path to a deprecated property.
  239. * @param {string} key - The name of the property.
  240. * @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
  241. * @returns {void}
  242. */
  243. function reportProperty(node, path, key, info) {
  244. const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
  245. path.push(key)
  246. const name = path.join(".")
  247. path.pop()
  248. if (ignored.indexOf(name) === -1) {
  249. report(node, `'${name}'`, info)
  250. }
  251. }
  252. /**
  253. * Checks violations in destructuring assignments.
  254. *
  255. * @param {ASTNode} node - A pattern node to check.
  256. * @param {string[]} path - The path to a deprecated property.
  257. * @param {object} infoMap - A map of properties' information.
  258. * @returns {void}
  259. */
  260. function checkDestructuring(node, path, infoMap) {
  261. switch (node.type) {
  262. case "AssignmentPattern":
  263. checkDestructuring(node.left, path, infoMap)
  264. break
  265. case "Identifier": {
  266. const variable = findVariable(node, globalScope)
  267. if (variable != null) {
  268. checkVariable(variable, path, infoMap)
  269. }
  270. break
  271. }
  272. case "ObjectPattern":
  273. for (const property of node.properties) {
  274. const key = getPropertyName(property)
  275. if (key != null && hasOwnProperty.call(infoMap, key)) {
  276. const keyInfo = infoMap[key]
  277. if (keyInfo.$deprecated) {
  278. reportProperty(property.key, path, key, keyInfo)
  279. }
  280. else {
  281. path.push(key)
  282. checkDestructuring(property.value, path, keyInfo)
  283. path.pop()
  284. }
  285. }
  286. }
  287. break
  288. // no default
  289. }
  290. }
  291. /**
  292. * Checks violations in properties.
  293. *
  294. * @param {ASTNode} root - A node to check.
  295. * @param {string[]} path - The path to a deprecated property.
  296. * @param {object} infoMap - A map of properties' information.
  297. * @returns {void}
  298. */
  299. function checkProperties(root, path, infoMap) { //eslint-disable-line complexity
  300. let node = root
  301. while (!SENTINEL_TYPE.test(node.parent.type)) {
  302. node = node.parent
  303. }
  304. const parent = node.parent
  305. switch (parent.type) {
  306. case "CallExpression":
  307. if (parent.callee === node && infoMap.$call != null) {
  308. reportCall(parent, path, infoMap.$call)
  309. }
  310. break
  311. case "NewExpression":
  312. if (parent.callee === node && infoMap.$constructor != null) {
  313. reportConstructor(parent, path, infoMap.$constructor)
  314. }
  315. break
  316. case "MemberExpression":
  317. if (parent.object === node) {
  318. const key = getPropertyName(parent)
  319. if (key != null && hasOwnProperty.call(infoMap, key)) {
  320. const keyInfo = infoMap[key]
  321. if (keyInfo.$deprecated) {
  322. reportProperty(parent.property, path, key, keyInfo)
  323. }
  324. else {
  325. path.push(key)
  326. checkProperties(parent, path, keyInfo)
  327. path.pop()
  328. }
  329. }
  330. }
  331. break
  332. case "AssignmentExpression":
  333. if (parent.right === node) {
  334. checkDestructuring(parent.left, path, infoMap)
  335. checkProperties(parent, path, infoMap)
  336. }
  337. break
  338. case "AssignmentPattern":
  339. if (parent.right === node) {
  340. checkDestructuring(parent.left, path, infoMap)
  341. }
  342. break
  343. case "VariableDeclarator":
  344. if (parent.init === node) {
  345. checkDestructuring(parent.id, path, infoMap)
  346. }
  347. break
  348. // no default
  349. }
  350. }
  351. /**
  352. * Checks violations in the references of a given variable.
  353. *
  354. * @param {escope.Variable} variable - A variable to check.
  355. * @param {string[]} path - The path to a deprecated property.
  356. * @param {object} infoMap - A map of properties' information.
  357. * @returns {void}
  358. */
  359. function checkVariable(variable, path, infoMap) {
  360. if (varStack.indexOf(variable) !== -1) {
  361. return
  362. }
  363. varStack.push(variable)
  364. if (infoMap.$deprecated) {
  365. const key = path.pop()
  366. for (const reference of variable.references.filter(r => r.isRead())) {
  367. reportProperty(reference.identifier, path, key, infoMap)
  368. }
  369. }
  370. else {
  371. for (const reference of variable.references.filter(r => r.isRead())) {
  372. checkProperties(reference.identifier, path, infoMap)
  373. }
  374. }
  375. varStack.pop()
  376. }
  377. /**
  378. * Checks violations in a ModuleSpecifier node.
  379. *
  380. * @param {ASTNode} node - A ModuleSpecifier node to check.
  381. * @param {string[]} path - The path to a deprecated property.
  382. * @param {object} infoMap - A map of properties' information.
  383. * @returns {void}
  384. */
  385. function checkImportSpecifier(node, path, infoMap) {
  386. switch (node.type) {
  387. case "ImportSpecifier": {
  388. const key = node.imported.name
  389. if (hasOwnProperty.call(infoMap, key)) {
  390. const keyInfo = infoMap[key]
  391. if (keyInfo.$deprecated) {
  392. reportProperty(node.imported, path, key, keyInfo)
  393. }
  394. else {
  395. path.push(key)
  396. checkVariable(
  397. findVariable(node.local, globalScope),
  398. path,
  399. keyInfo
  400. )
  401. path.pop()
  402. }
  403. }
  404. break
  405. }
  406. case "ImportDefaultSpecifier":
  407. checkVariable(
  408. findVariable(node.local, globalScope),
  409. path,
  410. infoMap
  411. )
  412. break
  413. case "ImportNamespaceSpecifier":
  414. checkVariable(
  415. findVariable(node.local, globalScope),
  416. path,
  417. Object.assign({}, infoMap, { default: infoMap })
  418. )
  419. break
  420. // no default
  421. }
  422. }
  423. /**
  424. * Checks violations for CommonJS modules.
  425. * @returns {void}
  426. */
  427. function checkCommonJsModules() {
  428. const infoMap = deprecatedApis.modules
  429. const variable = globalScope.set.get("require")
  430. if (variable == null || variable.defs.length !== 0) {
  431. return
  432. }
  433. for (const reference of variable.references.filter(r => r.isRead())) {
  434. const id = reference.identifier
  435. const node = id.parent
  436. if (node.type === "CallExpression" && node.callee === id) {
  437. const key = getValueIfString(node.arguments[0])
  438. if (key != null && hasOwnProperty.call(infoMap, key)) {
  439. const moduleInfo = infoMap[key]
  440. if (moduleInfo.$deprecated) {
  441. reportModule(node, key, moduleInfo)
  442. }
  443. else {
  444. checkProperties(node, [key], moduleInfo)
  445. }
  446. }
  447. }
  448. }
  449. }
  450. /**
  451. * Checks violations for ES2015 modules.
  452. * @param {ASTNode} programNode - A program node to check.
  453. * @returns {void}
  454. */
  455. function checkES2015Modules(programNode) {
  456. const infoMap = deprecatedApis.modules
  457. for (const node of programNode.body.filter(isImportDeclaration)) {
  458. const key = node.source.value
  459. if (hasOwnProperty.call(infoMap, key)) {
  460. const moduleInfo = infoMap[key]
  461. if (moduleInfo.$deprecated) {
  462. reportModule(node, key, moduleInfo)
  463. }
  464. else {
  465. for (const specifier of node.specifiers) {
  466. checkImportSpecifier(specifier, [key], moduleInfo)
  467. }
  468. }
  469. }
  470. }
  471. }
  472. /**
  473. * Checks violations for global variables.
  474. * @returns {void}
  475. */
  476. function checkGlobals() {
  477. const infoMap = deprecatedApis.globals
  478. for (const key of Object.keys(infoMap)) {
  479. const keyInfo = infoMap[key]
  480. const variable = globalScope.set.get(key)
  481. if (variable != null && variable.defs.length === 0) {
  482. checkVariable(variable, [key], keyInfo)
  483. }
  484. }
  485. }
  486. return {
  487. "Program:exit"(node) {
  488. globalScope = context.getScope()
  489. checkCommonJsModules()
  490. checkES2015Modules(node)
  491. checkGlobals()
  492. },
  493. }
  494. }
  495. //------------------------------------------------------------------------------
  496. // Rule Definition
  497. //------------------------------------------------------------------------------
  498. module.exports = {
  499. create,
  500. meta: {
  501. docs: {
  502. description: "disallow deprecated APIs",
  503. category: "Best Practices",
  504. recommended: true,
  505. url: getDocsUrl("no-deprecated-api.md"),
  506. },
  507. fixable: false,
  508. schema: [
  509. {
  510. type: "object",
  511. properties: {
  512. ignoreModuleItems: {
  513. type: "array",
  514. items: { enum: MODULE_ITEMS },
  515. additionalItems: false,
  516. uniqueItems: true,
  517. },
  518. ignoreGlobalItems: {
  519. type: "array",
  520. items: { enum: GLOBAL_ITEMS },
  521. additionalItems: false,
  522. uniqueItems: true,
  523. },
  524. // Deprecated since v4.2.0
  525. ignoreIndirectDependencies: { type: "boolean" },
  526. },
  527. additionalProperties: false,
  528. },
  529. ],
  530. },
  531. }