shebang.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /**
  2. * @author Toru Nagashima
  3. * See LICENSE file in root directory for full license.
  4. */
  5. "use strict"
  6. const path = require("path")
  7. const getConvertPath = require("../util/get-convert-path")
  8. const getPackageJson = require("../util/get-package-json")
  9. const NODE_SHEBANG = "#!/usr/bin/env node\n"
  10. const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
  11. const NODE_SHEBANG_PATTERN = /#!\/usr\/bin\/env node(?: [^\r\n]+?)?\n/u
  12. function simulateNodeResolutionAlgorithm(filePath, binField) {
  13. const possibilities = [filePath]
  14. let newFilePath = filePath.replace(/\.js$/u, "")
  15. possibilities.push(newFilePath)
  16. newFilePath = newFilePath.replace(/[/\\]index$/u, "")
  17. possibilities.push(newFilePath)
  18. return possibilities.includes(binField)
  19. }
  20. /**
  21. * Checks whether or not a given path is a `bin` file.
  22. *
  23. * @param {string} filePath - A file path to check.
  24. * @param {string|object|undefined} binField - A value of the `bin` field of `package.json`.
  25. * @param {string} basedir - A directory path that `package.json` exists.
  26. * @returns {boolean} `true` if the file is a `bin` file.
  27. */
  28. function isBinFile(filePath, binField, basedir) {
  29. if (!binField) {
  30. return false
  31. }
  32. if (typeof binField === "string") {
  33. return simulateNodeResolutionAlgorithm(
  34. filePath,
  35. path.resolve(basedir, binField)
  36. )
  37. }
  38. return Object.keys(binField).some(key =>
  39. simulateNodeResolutionAlgorithm(
  40. filePath,
  41. path.resolve(basedir, binField[key])
  42. )
  43. )
  44. }
  45. /**
  46. * Gets the shebang line (includes a line ending) from a given code.
  47. *
  48. * @param {SourceCode} sourceCode - A source code object to check.
  49. * @returns {{length: number, bom: boolean, shebang: string, cr: boolean}}
  50. * shebang's information.
  51. * `retv.shebang` is an empty string if shebang doesn't exist.
  52. */
  53. function getShebangInfo(sourceCode) {
  54. const m = SHEBANG_PATTERN.exec(sourceCode.text)
  55. return {
  56. bom: sourceCode.hasBOM,
  57. cr: Boolean(m && m[2]),
  58. length: (m && m[0].length) || 0,
  59. shebang: (m && m[1] && `${m[1]}\n`) || "",
  60. }
  61. }
  62. module.exports = {
  63. meta: {
  64. docs: {
  65. description: "require correct usage of shebang",
  66. category: "Possible Errors",
  67. recommended: true,
  68. url: "https://github.com/weiran-zsd/eslint-plugin-node/blob/HEAD/docs/rules/shebang.md",
  69. },
  70. type: "problem",
  71. fixable: "code",
  72. schema: [
  73. {
  74. type: "object",
  75. properties: {
  76. //
  77. convertPath: getConvertPath.schema,
  78. },
  79. additionalProperties: false,
  80. },
  81. ],
  82. messages: {
  83. unexpectedBOM: "This file must not have Unicode BOM.",
  84. expectedLF: "This file must have Unix linebreaks (LF).",
  85. expectedHashbangNode:
  86. 'This file needs shebang "#!/usr/bin/env node".',
  87. expectedHashbang: "This file needs no shebang.",
  88. },
  89. },
  90. create(context) {
  91. const sourceCode = context.getSourceCode()
  92. let filePath = context.getFilename()
  93. if (filePath === "<input>") {
  94. return {}
  95. }
  96. filePath = path.resolve(filePath)
  97. const p = getPackageJson(filePath)
  98. if (!p) {
  99. return {}
  100. }
  101. const basedir = path.dirname(p.filePath)
  102. filePath = path.join(
  103. basedir,
  104. getConvertPath(context)(
  105. path.relative(basedir, filePath).replace(/\\/gu, "/")
  106. )
  107. )
  108. const needsShebang = isBinFile(filePath, p.bin, basedir)
  109. const info = getShebangInfo(sourceCode)
  110. return {
  111. Program(node) {
  112. if (
  113. needsShebang
  114. ? NODE_SHEBANG_PATTERN.test(info.shebang)
  115. : !info.shebang
  116. ) {
  117. // Good the shebang target.
  118. // Checks BOM and \r.
  119. if (needsShebang && info.bom) {
  120. context.report({
  121. node,
  122. messageId: "unexpectedBOM",
  123. fix(fixer) {
  124. return fixer.removeRange([-1, 0])
  125. },
  126. })
  127. }
  128. if (needsShebang && info.cr) {
  129. context.report({
  130. node,
  131. messageId: "expectedLF",
  132. fix(fixer) {
  133. const index = sourceCode.text.indexOf("\r")
  134. return fixer.removeRange([index, index + 1])
  135. },
  136. })
  137. }
  138. } else if (needsShebang) {
  139. // Shebang is lacking.
  140. context.report({
  141. node,
  142. messageId: "expectedHashbangNode",
  143. fix(fixer) {
  144. return fixer.replaceTextRange(
  145. [-1, info.length],
  146. NODE_SHEBANG
  147. )
  148. },
  149. })
  150. } else {
  151. // Shebang is extra.
  152. context.report({
  153. node,
  154. messageId: "expectedHashbang",
  155. fix(fixer) {
  156. return fixer.removeRange([0, info.length])
  157. },
  158. })
  159. }
  160. },
  161. }
  162. },
  163. }