shebang.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /**
  2. * @author Toru Nagashima
  3. * @copyright 2015 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. "use strict"
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const path = require("path")
  11. const getConvertPath = require("../util/get-convert-path")
  12. const getDocsUrl = require("../util/get-docs-url")
  13. const getPackageJson = require("../util/get-package-json")
  14. //------------------------------------------------------------------------------
  15. // Helpers
  16. //------------------------------------------------------------------------------
  17. const NODE_SHEBANG = "#!/usr/bin/env node\n"
  18. const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/
  19. const NODE_SHEBANG_PATTERN = /#!\/usr\/bin\/env node(?: [^\r\n]+?)?\n/
  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 filePath === path.resolve(basedir, binField)
  34. }
  35. return Object.keys(binField).some(key => filePath === path.resolve(basedir, binField[key]))
  36. }
  37. /**
  38. * Gets the shebang line (includes a line ending) from a given code.
  39. *
  40. * @param {SourceCode} sourceCode - A source code object to check.
  41. * @returns {{length: number, bom: boolean, shebang: string, cr: boolean}}
  42. * shebang's information.
  43. * `retv.shebang` is an empty string if shebang doesn't exist.
  44. */
  45. function getShebangInfo(sourceCode) {
  46. const m = SHEBANG_PATTERN.exec(sourceCode.text)
  47. return {
  48. bom: sourceCode.hasBOM,
  49. cr: Boolean(m && m[2]),
  50. length: (m && m[0].length) || 0,
  51. shebang: (m && m[1] && (`${m[1]}\n`)) || "",
  52. }
  53. }
  54. /**
  55. * The definition of this rule.
  56. *
  57. * @param {RuleContext} context - The rule context to check.
  58. * @returns {object} The definition of this rule.
  59. */
  60. function create(context) {
  61. const sourceCode = context.getSourceCode()
  62. let filePath = context.getFilename()
  63. if (filePath === "<input>") {
  64. return {}
  65. }
  66. filePath = path.resolve(filePath)
  67. const p = getPackageJson(filePath)
  68. if (!p) {
  69. return {}
  70. }
  71. const basedir = path.dirname(p.filePath)
  72. filePath = path.join(
  73. basedir,
  74. getConvertPath(context)(path.relative(basedir, filePath).replace(/\\/g, "/"))
  75. )
  76. const needsShebang = isBinFile(filePath, p.bin, basedir)
  77. const info = getShebangInfo(sourceCode)
  78. return {
  79. Program(node) {
  80. if (needsShebang ? NODE_SHEBANG_PATTERN.test(info.shebang) : !info.shebang) {
  81. // Good the shebang target.
  82. // Checks BOM and \r.
  83. if (needsShebang && info.bom) {
  84. context.report({
  85. node,
  86. message: "This file must not have Unicode BOM.",
  87. fix(fixer) {
  88. return fixer.removeRange([-1, 0])
  89. },
  90. })
  91. }
  92. if (needsShebang && info.cr) {
  93. context.report({
  94. node,
  95. message: "This file must have Unix linebreaks (LF).",
  96. fix(fixer) {
  97. const index = sourceCode.text.indexOf("\r")
  98. return fixer.removeRange([index, index + 1])
  99. },
  100. })
  101. }
  102. }
  103. else if (needsShebang) {
  104. // Shebang is lacking.
  105. context.report({
  106. node,
  107. message: "This file needs shebang \"#!/usr/bin/env node\".",
  108. fix(fixer) {
  109. return fixer.replaceTextRange([-1, info.length], NODE_SHEBANG)
  110. },
  111. })
  112. }
  113. else {
  114. // Shebang is extra.
  115. context.report({
  116. node,
  117. message: "This file needs no shebang.",
  118. fix(fixer) {
  119. return fixer.removeRange([0, info.length])
  120. },
  121. })
  122. }
  123. },
  124. }
  125. }
  126. //------------------------------------------------------------------------------
  127. // Rule Definition
  128. //------------------------------------------------------------------------------
  129. module.exports = {
  130. create,
  131. meta: {
  132. docs: {
  133. description: "enforce the correct usage of shebang",
  134. category: "Possible Errors",
  135. recommended: true,
  136. url: getDocsUrl("shebang.md"),
  137. },
  138. fixable: "code",
  139. schema: [
  140. {
  141. type: "object",
  142. properties: { //
  143. convertPath: getConvertPath.schema,
  144. },
  145. additionalProperties: false,
  146. },
  147. ],
  148. },
  149. }