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