no-path-concat.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * @author Nicholas C. Zakas
  3. * See LICENSE file in root directory for full license.
  4. */
  5. "use strict"
  6. const path = require("path")
  7. const { READ, ReferenceTracker, getStringIfConstant } = require("eslint-utils")
  8. /**
  9. * Get the first char of the specified template element.
  10. * @param {TemplateLiteral} node The `TemplateLiteral` node to get.
  11. * @param {number} i The number of template elements to get first char.
  12. * @param {Set<Node>} sepNodes The nodes of `path.sep`.
  13. * @param {import("escope").Scope} globalScope The global scope object.
  14. * @param {string[]} outNextChars The array to collect chars.
  15. * @returns {void}
  16. */
  17. function collectFirstCharsOfTemplateElement(
  18. node,
  19. i,
  20. sepNodes,
  21. globalScope,
  22. outNextChars
  23. ) {
  24. const element = node.quasis[i].value.cooked
  25. if (element == null) {
  26. return
  27. }
  28. if (element !== "") {
  29. outNextChars.push(element[0])
  30. return
  31. }
  32. if (node.expressions.length > i) {
  33. collectFirstChars(
  34. node.expressions[i],
  35. sepNodes,
  36. globalScope,
  37. outNextChars
  38. )
  39. }
  40. }
  41. /**
  42. * Get the first char of a given node.
  43. * @param {TemplateLiteral} node The `TemplateLiteral` node to get.
  44. * @param {Set<Node>} sepNodes The nodes of `path.sep`.
  45. * @param {import("escope").Scope} globalScope The global scope object.
  46. * @param {string[]} outNextChars The array to collect chars.
  47. * @returns {void}
  48. */
  49. function collectFirstChars(node, sepNodes, globalScope, outNextChars) {
  50. switch (node.type) {
  51. case "AssignmentExpression":
  52. collectFirstChars(node.right, sepNodes, globalScope, outNextChars)
  53. break
  54. case "BinaryExpression":
  55. collectFirstChars(node.left, sepNodes, globalScope, outNextChars)
  56. break
  57. case "ConditionalExpression":
  58. collectFirstChars(
  59. node.consequent,
  60. sepNodes,
  61. globalScope,
  62. outNextChars
  63. )
  64. collectFirstChars(
  65. node.alternate,
  66. sepNodes,
  67. globalScope,
  68. outNextChars
  69. )
  70. break
  71. case "LogicalExpression":
  72. collectFirstChars(node.left, sepNodes, globalScope, outNextChars)
  73. collectFirstChars(node.right, sepNodes, globalScope, outNextChars)
  74. break
  75. case "SequenceExpression":
  76. collectFirstChars(
  77. node.expressions[node.expressions.length - 1],
  78. sepNodes,
  79. globalScope,
  80. outNextChars
  81. )
  82. break
  83. case "TemplateLiteral":
  84. collectFirstCharsOfTemplateElement(
  85. node,
  86. 0,
  87. sepNodes,
  88. globalScope,
  89. outNextChars
  90. )
  91. break
  92. case "Identifier":
  93. case "MemberExpression":
  94. if (sepNodes.has(node)) {
  95. outNextChars.push(path.sep)
  96. break
  97. }
  98. // fallthrough
  99. default: {
  100. const str = getStringIfConstant(node, globalScope)
  101. if (str) {
  102. outNextChars.push(str[0])
  103. }
  104. }
  105. }
  106. }
  107. /**
  108. * Check if a char is a path separator or not.
  109. * @param {string} c The char to check.
  110. * @returns {boolean} `true` if the char is a path separator.
  111. */
  112. function isPathSeparator(c) {
  113. return c === "/" || c === path.sep
  114. }
  115. /**
  116. * Check if the given Identifier node is followed by string concatenation with a
  117. * path separator.
  118. * @param {Identifier} node The `__dirname` or `__filename` node to check.
  119. * @param {Set<Node>} sepNodes The nodes of `path.sep`.
  120. * @param {import("escope").Scope} globalScope The global scope object.
  121. * @returns {boolean} `true` if the given Identifier node is followed by string
  122. * concatenation with a path separator.
  123. */
  124. function isConcat(node, sepNodes, globalScope) {
  125. const parent = node.parent
  126. const nextChars = []
  127. if (
  128. parent.type === "BinaryExpression" &&
  129. parent.operator === "+" &&
  130. parent.left === node
  131. ) {
  132. collectFirstChars(
  133. parent.right,
  134. sepNodes,
  135. globalScope,
  136. /* out */ nextChars
  137. )
  138. } else if (parent.type === "TemplateLiteral") {
  139. collectFirstCharsOfTemplateElement(
  140. parent,
  141. parent.expressions.indexOf(node) + 1,
  142. sepNodes,
  143. globalScope,
  144. /* out */ nextChars
  145. )
  146. }
  147. return nextChars.some(isPathSeparator)
  148. }
  149. module.exports = {
  150. meta: {
  151. type: "suggestion",
  152. docs: {
  153. description:
  154. "disallow string concatenation with `__dirname` and `__filename`",
  155. category: "Possible Errors",
  156. recommended: false,
  157. url:
  158. "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/no-path-concat.md",
  159. },
  160. fixable: null,
  161. schema: [],
  162. messages: {
  163. usePathFunctions:
  164. "Use path.join() or path.resolve() instead of string concatenation.",
  165. },
  166. },
  167. create(context) {
  168. return {
  169. "Program:exit"() {
  170. const globalScope = context.getScope()
  171. const tracker = new ReferenceTracker(globalScope)
  172. const sepNodes = new Set()
  173. // Collect `paht.sep` references
  174. for (const { node } of tracker.iterateCjsReferences({
  175. path: { sep: { [READ]: true } },
  176. })) {
  177. sepNodes.add(node)
  178. }
  179. for (const { node } of tracker.iterateEsmReferences({
  180. path: { sep: { [READ]: true } },
  181. })) {
  182. sepNodes.add(node)
  183. }
  184. // Verify `__dirname` and `__filename`
  185. for (const { node } of tracker.iterateGlobalReferences({
  186. __dirname: { [READ]: true },
  187. __filename: { [READ]: true },
  188. })) {
  189. if (isConcat(node, sepNodes, globalScope)) {
  190. context.report({
  191. node: node.parent,
  192. messageId: "usePathFunctions",
  193. })
  194. }
  195. }
  196. },
  197. }
  198. },
  199. }