shebang.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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: "suggest correct usage of shebang",
  66. category: "Possible Errors",
  67. recommended: true,
  68. url:
  69. "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/shebang.md",
  70. },
  71. type: "problem",
  72. fixable: "code",
  73. schema: [
  74. {
  75. type: "object",
  76. properties: {
  77. //
  78. convertPath: getConvertPath.schema,
  79. },
  80. additionalProperties: false,
  81. },
  82. ],
  83. },
  84. create(context) {
  85. const sourceCode = context.getSourceCode()
  86. let filePath = context.getFilename()
  87. if (filePath === "<input>") {
  88. return {}
  89. }
  90. filePath = path.resolve(filePath)
  91. const p = getPackageJson(filePath)
  92. if (!p) {
  93. return {}
  94. }
  95. const basedir = path.dirname(p.filePath)
  96. filePath = path.join(
  97. basedir,
  98. getConvertPath(context)(
  99. path.relative(basedir, filePath).replace(/\\/gu, "/")
  100. )
  101. )
  102. const needsShebang = isBinFile(filePath, p.bin, basedir)
  103. const info = getShebangInfo(sourceCode)
  104. return {
  105. Program(node) {
  106. if (
  107. needsShebang
  108. ? NODE_SHEBANG_PATTERN.test(info.shebang)
  109. : !info.shebang
  110. ) {
  111. // Good the shebang target.
  112. // Checks BOM and \r.
  113. if (needsShebang && info.bom) {
  114. context.report({
  115. node,
  116. message: "This file must not have Unicode BOM.",
  117. fix(fixer) {
  118. return fixer.removeRange([-1, 0])
  119. },
  120. })
  121. }
  122. if (needsShebang && info.cr) {
  123. context.report({
  124. node,
  125. message:
  126. "This file must have Unix linebreaks (LF).",
  127. fix(fixer) {
  128. const index = sourceCode.text.indexOf("\r")
  129. return fixer.removeRange([index, index + 1])
  130. },
  131. })
  132. }
  133. } else if (needsShebang) {
  134. // Shebang is lacking.
  135. context.report({
  136. node,
  137. message:
  138. 'This file needs shebang "#!/usr/bin/env node".',
  139. fix(fixer) {
  140. return fixer.replaceTextRange(
  141. [-1, info.length],
  142. NODE_SHEBANG
  143. )
  144. },
  145. })
  146. } else {
  147. // Shebang is extra.
  148. context.report({
  149. node,
  150. message: "This file needs no shebang.",
  151. fix(fixer) {
  152. return fixer.removeRange([0, info.length])
  153. },
  154. })
  155. }
  156. },
  157. }
  158. },
  159. }