shebang.js 5.1 KB

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