file-extension-in-import.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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 fs = require("fs")
  8. const getTryExtensions = require("../util/get-try-extensions")
  9. const visitImport = require("../util/visit-import")
  10. const packageNamePattern = /^(?:@[^/\\]+[/\\])?[^/\\]+$/u
  11. const corePackageOverridePattern = /^(?:assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|worker_threads|zlib)[/\\]$/u
  12. /**
  13. * Get all file extensions of the files which have the same basename.
  14. * @param {string} filePath The path to the original file to check.
  15. * @returns {string[]} File extensions.
  16. */
  17. function getExistingExtensions(filePath) {
  18. const basename = path.basename(filePath, path.extname(filePath))
  19. try {
  20. return fs
  21. .readdirSync(path.dirname(filePath))
  22. .filter(
  23. filename =>
  24. path.basename(filename, path.extname(filename)) === basename
  25. )
  26. .map(filename => path.extname(filename))
  27. } catch (_error) {
  28. return []
  29. }
  30. }
  31. module.exports = {
  32. meta: {
  33. docs: {
  34. description:
  35. "enforce the style of file extensions in `import` declarations",
  36. category: "Stylistic Issues",
  37. recommended: false,
  38. url:
  39. "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/file-extension-in-import.md",
  40. },
  41. fixable: "code",
  42. messages: {
  43. requireExt: "require file extension '{{ext}}'.",
  44. forbidExt: "forbid file extension '{{ext}}'.",
  45. },
  46. schema: [
  47. {
  48. enum: ["always", "never"],
  49. },
  50. {
  51. type: "object",
  52. properties: {
  53. tryExtensions: getTryExtensions.schema,
  54. },
  55. additionalProperties: {
  56. enum: ["always", "never"],
  57. },
  58. },
  59. ],
  60. type: "suggestion",
  61. },
  62. create(context) {
  63. if (context.getFilename().startsWith("<")) {
  64. return {}
  65. }
  66. const defaultStyle = context.options[0] || "always"
  67. const overrideStyle = context.options[1] || {}
  68. function verify({ filePath, name, node }) {
  69. // Ignore if it's not resolved to a file or it's a bare module.
  70. if (
  71. !filePath ||
  72. packageNamePattern.test(name) ||
  73. corePackageOverridePattern.test(name)
  74. ) {
  75. return
  76. }
  77. // Get extension.
  78. const originalExt = path.extname(name)
  79. const resolvedExt = path.extname(filePath)
  80. const existingExts = getExistingExtensions(filePath)
  81. if (!resolvedExt && existingExts.length !== 1) {
  82. // Ignore if the file extension could not be determined one.
  83. return
  84. }
  85. const ext = resolvedExt || existingExts[0]
  86. const style = overrideStyle[ext] || defaultStyle
  87. // Verify.
  88. if (style === "always" && ext !== originalExt) {
  89. context.report({
  90. node,
  91. messageId: "requireExt",
  92. data: { ext },
  93. fix(fixer) {
  94. if (existingExts.length !== 1) {
  95. return null
  96. }
  97. const index = node.range[1] - 1
  98. return fixer.insertTextBeforeRange([index, index], ext)
  99. },
  100. })
  101. } else if (style === "never" && ext === originalExt) {
  102. context.report({
  103. node,
  104. messageId: "forbidExt",
  105. data: { ext },
  106. fix(fixer) {
  107. if (existingExts.length !== 1) {
  108. return null
  109. }
  110. const index = name.lastIndexOf(ext)
  111. const start = node.range[0] + 1 + index
  112. const end = start + ext.length
  113. return fixer.removeRange([start, end])
  114. },
  115. })
  116. }
  117. }
  118. return visitImport(context, { optionIndex: 1 }, targets => {
  119. targets.forEach(verify)
  120. })
  121. },
  122. }