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.
297 lines
8.2 KiB
297 lines
8.2 KiB
/** |
|
* @author Toru Nagashima |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
"use strict" |
|
|
|
/*istanbul ignore next */ |
|
/** |
|
* This function is copied from https://github.com/eslint/eslint/blob/2355f8d0de1d6732605420d15ddd4f1eee3c37b6/lib/ast-utils.js#L648-L684 |
|
* |
|
* @param {ASTNode} node - The node to get. |
|
* @returns {string|null} The property name if static. Otherwise, null. |
|
* @private |
|
*/ |
|
function getStaticPropertyName(node) { |
|
let prop = null |
|
|
|
switch (node && node.type) { |
|
case "Property": |
|
case "MethodDefinition": |
|
prop = node.key |
|
break |
|
|
|
case "MemberExpression": |
|
prop = node.property |
|
break |
|
|
|
// no default |
|
} |
|
|
|
switch (prop && prop.type) { |
|
case "Literal": |
|
return String(prop.value) |
|
|
|
case "TemplateLiteral": |
|
if (prop.expressions.length === 0 && prop.quasis.length === 1) { |
|
return prop.quasis[0].value.cooked |
|
} |
|
break |
|
|
|
case "Identifier": |
|
if (!node.computed) { |
|
return prop.name |
|
} |
|
break |
|
|
|
// no default |
|
} |
|
|
|
return null |
|
} |
|
|
|
/** |
|
* Checks whether the given node is assignee or not. |
|
* |
|
* @param {ASTNode} node - The node to check. |
|
* @returns {boolean} `true` if the node is assignee. |
|
*/ |
|
function isAssignee(node) { |
|
return ( |
|
node.parent.type === "AssignmentExpression" && node.parent.left === node |
|
) |
|
} |
|
|
|
/** |
|
* Gets the top assignment expression node if the given node is an assignee. |
|
* |
|
* This is used to distinguish 2 assignees belong to the same assignment. |
|
* If the node is not an assignee, this returns null. |
|
* |
|
* @param {ASTNode} leafNode - The node to get. |
|
* @returns {ASTNode|null} The top assignment expression node, or null. |
|
*/ |
|
function getTopAssignment(leafNode) { |
|
let node = leafNode |
|
|
|
// Skip MemberExpressions. |
|
while ( |
|
node.parent.type === "MemberExpression" && |
|
node.parent.object === node |
|
) { |
|
node = node.parent |
|
} |
|
|
|
// Check assignments. |
|
if (!isAssignee(node)) { |
|
return null |
|
} |
|
|
|
// Find the top. |
|
while (node.parent.type === "AssignmentExpression") { |
|
node = node.parent |
|
} |
|
|
|
return node |
|
} |
|
|
|
/** |
|
* Gets top assignment nodes of the given node list. |
|
* |
|
* @param {ASTNode[]} nodes - The node list to get. |
|
* @returns {ASTNode[]} Gotten top assignment nodes. |
|
*/ |
|
function createAssignmentList(nodes) { |
|
return nodes.map(getTopAssignment).filter(Boolean) |
|
} |
|
|
|
/** |
|
* Gets the reference of `module.exports` from the given scope. |
|
* |
|
* @param {escope.Scope} scope - The scope to get. |
|
* @returns {ASTNode[]} Gotten MemberExpression node list. |
|
*/ |
|
function getModuleExportsNodes(scope) { |
|
const variable = scope.set.get("module") |
|
if (variable == null) { |
|
return [] |
|
} |
|
return variable.references |
|
.map(reference => reference.identifier.parent) |
|
.filter( |
|
node => |
|
node.type === "MemberExpression" && |
|
getStaticPropertyName(node) === "exports" |
|
) |
|
} |
|
|
|
/** |
|
* Gets the reference of `exports` from the given scope. |
|
* |
|
* @param {escope.Scope} scope - The scope to get. |
|
* @returns {ASTNode[]} Gotten Identifier node list. |
|
*/ |
|
function getExportsNodes(scope) { |
|
const variable = scope.set.get("exports") |
|
if (variable == null) { |
|
return [] |
|
} |
|
return variable.references.map(reference => reference.identifier) |
|
} |
|
|
|
module.exports = { |
|
meta: { |
|
docs: { |
|
description: "enforce either `module.exports` or `exports`", |
|
category: "Stylistic Issues", |
|
recommended: false, |
|
url: |
|
"https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/exports-style.md", |
|
}, |
|
type: "suggestion", |
|
fixable: null, |
|
schema: [ |
|
{ |
|
// |
|
enum: ["module.exports", "exports"], |
|
}, |
|
{ |
|
type: "object", |
|
properties: { allowBatchAssign: { type: "boolean" } }, |
|
additionalProperties: false, |
|
}, |
|
], |
|
}, |
|
|
|
create(context) { |
|
const mode = context.options[0] || "module.exports" |
|
const batchAssignAllowed = Boolean( |
|
context.options[1] != null && context.options[1].allowBatchAssign |
|
) |
|
const sourceCode = context.getSourceCode() |
|
|
|
/** |
|
* Gets the location info of reports. |
|
* |
|
* exports = foo |
|
* ^^^^^^^^^ |
|
* |
|
* module.exports = foo |
|
* ^^^^^^^^^^^^^^^^ |
|
* |
|
* @param {ASTNode} node - The node of `exports`/`module.exports`. |
|
* @returns {Location} The location info of reports. |
|
*/ |
|
function getLocation(node) { |
|
const token = sourceCode.getTokenAfter(node) |
|
return { |
|
start: node.loc.start, |
|
end: token.loc.end, |
|
} |
|
} |
|
|
|
/** |
|
* Enforces `module.exports`. |
|
* This warns references of `exports`. |
|
* |
|
* @returns {void} |
|
*/ |
|
function enforceModuleExports() { |
|
const globalScope = context.getScope() |
|
const exportsNodes = getExportsNodes(globalScope) |
|
const assignList = batchAssignAllowed |
|
? createAssignmentList(getModuleExportsNodes(globalScope)) |
|
: [] |
|
|
|
for (const node of exportsNodes) { |
|
// Skip if it's a batch assignment. |
|
if ( |
|
assignList.length > 0 && |
|
assignList.indexOf(getTopAssignment(node)) !== -1 |
|
) { |
|
continue |
|
} |
|
|
|
// Report. |
|
context.report({ |
|
node, |
|
loc: getLocation(node), |
|
message: |
|
"Unexpected access to 'exports'. Use 'module.exports' instead.", |
|
}) |
|
} |
|
} |
|
|
|
/** |
|
* Enforces `exports`. |
|
* This warns references of `module.exports`. |
|
* |
|
* @returns {void} |
|
*/ |
|
function enforceExports() { |
|
const globalScope = context.getScope() |
|
const exportsNodes = getExportsNodes(globalScope) |
|
const moduleExportsNodes = getModuleExportsNodes(globalScope) |
|
const assignList = batchAssignAllowed |
|
? createAssignmentList(exportsNodes) |
|
: [] |
|
const batchAssignList = [] |
|
|
|
for (const node of moduleExportsNodes) { |
|
// Skip if it's a batch assignment. |
|
if (assignList.length > 0) { |
|
const found = assignList.indexOf(getTopAssignment(node)) |
|
if (found !== -1) { |
|
batchAssignList.push(assignList[found]) |
|
assignList.splice(found, 1) |
|
continue |
|
} |
|
} |
|
|
|
// Report. |
|
context.report({ |
|
node, |
|
loc: getLocation(node), |
|
message: |
|
"Unexpected access to 'module.exports'. Use 'exports' instead.", |
|
}) |
|
} |
|
|
|
// Disallow direct assignment to `exports`. |
|
for (const node of exportsNodes) { |
|
// Skip if it's not assignee. |
|
if (!isAssignee(node)) { |
|
continue |
|
} |
|
|
|
// Check if it's a batch assignment. |
|
if (batchAssignList.indexOf(getTopAssignment(node)) !== -1) { |
|
continue |
|
} |
|
|
|
// Report. |
|
context.report({ |
|
node, |
|
loc: getLocation(node), |
|
message: |
|
"Unexpected assignment to 'exports'. Don't modify 'exports' itself.", |
|
}) |
|
} |
|
} |
|
|
|
return { |
|
"Program:exit"() { |
|
switch (mode) { |
|
case "module.exports": |
|
enforceModuleExports() |
|
break |
|
case "exports": |
|
enforceExports() |
|
break |
|
|
|
// no default |
|
} |
|
}, |
|
} |
|
}, |
|
}
|
|
|