298 lines
8.2 KiB
JavaScript
298 lines
8.2 KiB
JavaScript
|
/**
|
||
|
* @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/v10.0.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
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
},
|
||
|
}
|