shebang.js 5.0 KB

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