/** * @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, }, ], }, }