blob: f6e1d7111ca92591409c71151b7cea50686e2cd3 [file] [log] [blame]
/**
* @fileoverview Rule to disallow deprecated API.
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict"
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const deprecatedApis = require("../util/deprecated-apis")
const getValueIfString = require("../util/get-value-if-string")
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const SENTINEL_TYPE = /^(?:.+?Statement|.+?Declaration|(?:Array|ArrowFunction|Assignment|Call|Class|Function|Member|New|Object)Expression|AssignmentPattern|Program|VariableDeclarator)$/
const MODULE_ITEMS = getDeprecatedItems(deprecatedApis.modules, [], [])
const GLOBAL_ITEMS = getDeprecatedItems(deprecatedApis.globals, [], [])
/**
* Gets the array of deprecated items.
*
* It's the paths which are separated by dots.
* E.g. `buffer.Buffer`, `events.EventEmitter.listenerCount`
*
* @param {object} definition - The definition of deprecated APIs.
* @param {string[]} result - The array of the result.
* @param {string[]} stack - The array to manage the stack of paths.
* @returns {string[]} `result`.
*/
function getDeprecatedItems(definition, result, stack) {
for (const key of Object.keys(definition)) {
const item = definition[key]
if (key === "$call") {
result.push(`${stack.join(".")}()`)
}
else if (key === "$constructor") {
result.push(`new ${stack.join(".")}()`)
}
else {
stack.push(key)
if (item.$deprecated) {
result.push(stack.join("."))
}
else {
getDeprecatedItems(item, result, stack)
}
stack.pop()
}
}
return result
}
/**
* Converts from a version number to a version text to display.
*
* @param {number} value - A version number to convert.
* @returns {string} Covnerted text.
*/
function toVersionText(value) {
if (value <= 0.12) {
return value.toFixed(2)
}
if (value < 1) {
return value.toFixed(1)
}
return String(value)
}
/**
* Makes a replacement message.
*
* @param {string|null} replacedBy - The text of substitute way.
* @returns {string} Replacement message.
*/
function toReplaceMessage(replacedBy) {
return replacedBy ? ` Use ${replacedBy} instead.` : ""
}
/**
* Gets the property name from a MemberExpression node or a Property node.
*
* @param {ASTNode} node - A node to get.
* @returns {string|null} The property name of the node.
*/
function getPropertyName(node) {
switch (node.type) {
case "MemberExpression":
if (node.computed) {
return getValueIfString(node.property)
}
return node.property.name
case "Property":
if (node.computed) {
return getValueIfString(node.key)
}
if (node.key.type === "Literal") {
return String(node.key.value)
}
return node.key.name
// no default
}
/* istanbul ignore next: unreachable */
return null
}
/**
* Checks a given node is a ImportDeclaration node.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a ImportDeclaration node.
*/
function isImportDeclaration(node) {
return node.type === "ImportDeclaration"
}
/**
* Finds the variable object of a given Identifier node.
*
* @param {ASTNode} node - An Identifier node to find.
* @param {escope.Scope} initialScope - A scope to start searching.
* @returns {escope.Variable} Found variable object.
*/
function findVariable(node, initialScope) {
const location = node.range[0]
let variable = null
// Dive into the scope that the node exists.
for (const childScope of initialScope.childScopes) {
const range = childScope.block.range
if (range[0] <= location && location < range[1]) {
variable = findVariable(node, childScope)
if (variable != null) {
return variable
}
}
}
// Find the variable of that name in this scope or ancestor scopes.
let scope = initialScope
while (scope != null) {
variable = scope.set.get(node.name)
if (variable != null) {
return variable
}
scope = scope.upper
}
return null
}
/**
* Gets the top member expression node.
*
* @param {ASTNode} identifier - The node to get.
* @returns {ASTNode} The top member expression node.
*/
function getTopMemberExpression(identifier) {
if (identifier.type !== "Identifier" && identifier.type !== "Literal") {
return identifier
}
let node = identifier
while (node.parent.type === "MemberExpression") {
node = node.parent
}
return node
}
/**
* The definition of this rule.
*
* @param {RuleContext} context - The rule context to check.
* @returns {object} The definition of this rule.
*/
function create(context) {
const options = context.options[0] || {}
const ignoredModuleItems = options.ignoreModuleItems || []
const ignoredGlobalItems = options.ignoreGlobalItems || []
let globalScope = null
const varStack = []
/**
* Reports a use of a deprecated API.
*
* @param {ASTNode} node - A node to report.
* @param {string} name - The name of a deprecated API.
* @param {{since: number, replacedBy: string}} info - Information of the API.
* @returns {void}
*/
function report(node, name, info) {
context.report({
node,
loc: getTopMemberExpression(node).loc,
message: "{{name}} was deprecated since v{{version}}.{{replace}}",
data: {
name,
version: toVersionText(info.since),
replace: toReplaceMessage(info.replacedBy),
},
})
}
/**
* Reports a use of a deprecated module.
*
* @param {ASTNode} node - A node to report.
* @param {string} name - The name of a deprecated module.
* @param {{since: number, replacedBy: string, global: boolean}} info - Information of the module.
* @returns {void}
*/
function reportModule(node, name, info) {
if (ignoredModuleItems.indexOf(name) === -1) {
report(node, `'${name}' module`, info)
}
}
/**
* Reports a use of a deprecated property.
*
* @param {ASTNode} node - A node to report.
* @param {string[]} path - The path to a deprecated property.
* @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
* @returns {void}
*/
function reportCall(node, path, info) {
const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
const name = `${path.join(".")}()`
if (ignored.indexOf(name) === -1) {
report(node, `'${name}'`, info)
}
}
/**
* Reports a use of a deprecated property.
*
* @param {ASTNode} node - A node to report.
* @param {string[]} path - The path to a deprecated property.
* @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
* @returns {void}
*/
function reportConstructor(node, path, info) {
const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
const name = `new ${path.join(".")}()`
if (ignored.indexOf(name) === -1) {
report(node, `'${name}'`, info)
}
}
/**
* Reports a use of a deprecated property.
*
* @param {ASTNode} node - A node to report.
* @param {string[]} path - The path to a deprecated property.
* @param {string} key - The name of the property.
* @param {{since: number, replacedBy: string, global: boolean}} info - Information of the property.
* @returns {void}
*/
function reportProperty(node, path, key, info) {
const ignored = info.global ? ignoredGlobalItems : ignoredModuleItems
path.push(key)
const name = path.join(".")
path.pop()
if (ignored.indexOf(name) === -1) {
report(node, `'${name}'`, info)
}
}
/**
* Checks violations in destructuring assignments.
*
* @param {ASTNode} node - A pattern node to check.
* @param {string[]} path - The path to a deprecated property.
* @param {object} infoMap - A map of properties' information.
* @returns {void}
*/
function checkDestructuring(node, path, infoMap) {
switch (node.type) {
case "AssignmentPattern":
checkDestructuring(node.left, path, infoMap)
break
case "Identifier": {
const variable = findVariable(node, globalScope)
if (variable != null) {
checkVariable(variable, path, infoMap)
}
break
}
case "ObjectPattern":
for (const property of node.properties) {
const key = getPropertyName(property)
if (key != null && hasOwnProperty.call(infoMap, key)) {
const keyInfo = infoMap[key]
if (keyInfo.$deprecated) {
reportProperty(property.key, path, key, keyInfo)
}
else {
path.push(key)
checkDestructuring(property.value, path, keyInfo)
path.pop()
}
}
}
break
// no default
}
}
/**
* Checks violations in properties.
*
* @param {ASTNode} root - A node to check.
* @param {string[]} path - The path to a deprecated property.
* @param {object} infoMap - A map of properties' information.
* @returns {void}
*/
function checkProperties(root, path, infoMap) { //eslint-disable-line complexity
let node = root
while (!SENTINEL_TYPE.test(node.parent.type)) {
node = node.parent
}
const parent = node.parent
switch (parent.type) {
case "CallExpression":
if (parent.callee === node && infoMap.$call != null) {
reportCall(parent, path, infoMap.$call)
}
break
case "NewExpression":
if (parent.callee === node && infoMap.$constructor != null) {
reportConstructor(parent, path, infoMap.$constructor)
}
break
case "MemberExpression":
if (parent.object === node) {
const key = getPropertyName(parent)
if (key != null && hasOwnProperty.call(infoMap, key)) {
const keyInfo = infoMap[key]
if (keyInfo.$deprecated) {
reportProperty(parent.property, path, key, keyInfo)
}
else {
path.push(key)
checkProperties(parent, path, keyInfo)
path.pop()
}
}
}
break
case "AssignmentExpression":
if (parent.right === node) {
checkDestructuring(parent.left, path, infoMap)
checkProperties(parent, path, infoMap)
}
break
case "AssignmentPattern":
if (parent.right === node) {
checkDestructuring(parent.left, path, infoMap)
}
break
case "VariableDeclarator":
if (parent.init === node) {
checkDestructuring(parent.id, path, infoMap)
}
break
// no default
}
}
/**
* Checks violations in the references of a given variable.
*
* @param {escope.Variable} variable - A variable to check.
* @param {string[]} path - The path to a deprecated property.
* @param {object} infoMap - A map of properties' information.
* @returns {void}
*/
function checkVariable(variable, path, infoMap) {
if (varStack.indexOf(variable) !== -1) {
return
}
varStack.push(variable)
if (infoMap.$deprecated) {
const key = path.pop()
for (const reference of variable.references.filter(r => r.isRead())) {
reportProperty(reference.identifier, path, key, infoMap)
}
}
else {
for (const reference of variable.references.filter(r => r.isRead())) {
checkProperties(reference.identifier, path, infoMap)
}
}
varStack.pop()
}
/**
* Checks violations in a ModuleSpecifier node.
*
* @param {ASTNode} node - A ModuleSpecifier node to check.
* @param {string[]} path - The path to a deprecated property.
* @param {object} infoMap - A map of properties' information.
* @returns {void}
*/
function checkImportSpecifier(node, path, infoMap) {
switch (node.type) {
case "ImportSpecifier": {
const key = node.imported.name
if (hasOwnProperty.call(infoMap, key)) {
const keyInfo = infoMap[key]
if (keyInfo.$deprecated) {
reportProperty(node.imported, path, key, keyInfo)
}
else {
path.push(key)
checkVariable(
findVariable(node.local, globalScope),
path,
keyInfo
)
path.pop()
}
}
break
}
case "ImportDefaultSpecifier":
checkVariable(
findVariable(node.local, globalScope),
path,
infoMap
)
break
case "ImportNamespaceSpecifier":
checkVariable(
findVariable(node.local, globalScope),
path,
Object.assign({}, infoMap, {default: infoMap})
)
break
// no default
}
}
/**
* Checks violations for CommonJS modules.
* @returns {void}
*/
function checkCommonJsModules() {
const infoMap = deprecatedApis.modules
const variable = globalScope.set.get("require")
if (variable == null || variable.defs.length !== 0) {
return
}
for (const reference of variable.references.filter(r => r.isRead())) {
const id = reference.identifier
const node = id.parent
if (node.type === "CallExpression" && node.callee === id) {
const key = getValueIfString(node.arguments[0])
if (key != null && hasOwnProperty.call(infoMap, key)) {
const moduleInfo = infoMap[key]
if (moduleInfo.$deprecated) {
reportModule(node, key, moduleInfo)
}
else {
checkProperties(node, [key], moduleInfo)
}
}
}
}
}
/**
* Checks violations for ES2015 modules.
* @param {ASTNode} programNode - A program node to check.
* @returns {void}
*/
function checkES2015Modules(programNode) {
const infoMap = deprecatedApis.modules
for (const node of programNode.body.filter(isImportDeclaration)) {
const key = node.source.value
if (hasOwnProperty.call(infoMap, key)) {
const moduleInfo = infoMap[key]
if (moduleInfo.$deprecated) {
reportModule(node, key, moduleInfo)
}
else {
for (const specifier of node.specifiers) {
checkImportSpecifier(specifier, [key], moduleInfo)
}
}
}
}
}
/**
* Checks violations for global variables.
* @returns {void}
*/
function checkGlobals() {
const infoMap = deprecatedApis.globals
for (const key of Object.keys(infoMap)) {
const keyInfo = infoMap[key]
const variable = globalScope.set.get(key)
if (variable != null && variable.defs.length === 0) {
checkVariable(variable, [key], keyInfo)
}
}
}
return {
"Program:exit"(node) {
globalScope = context.getScope()
checkCommonJsModules()
checkES2015Modules(node)
checkGlobals()
},
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
create,
meta: {
docs: {
description: "disallow deprecated APIs",
category: "Best Practices",
recommended: true,
},
fixable: false,
schema: [
{
type: "object",
properties: {
ignoreModuleItems: {
type: "array",
items: {enum: MODULE_ITEMS},
additionalItems: false,
uniqueItems: true,
},
ignoreGlobalItems: {
type: "array",
items: {enum: GLOBAL_ITEMS},
additionalItems: false,
uniqueItems: true,
},
// Deprecated since v4.2.0
ignoreIndirectDependencies: {type: "boolean"},
},
additionalProperties: false,
},
],
},
}