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