require-vi-doMock-valid-path.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. const path = require('node:path')
  2. const fs = require('node:fs')
  3. module.exports = {
  4. meta: {
  5. type: 'problem',
  6. docs: {
  7. description: 'Ensure vi.doMock first argument is a resolvable path.',
  8. category: 'Best Practices',
  9. recommended: false,
  10. url: '',
  11. },
  12. fixable: 'code',
  13. hasSuggestions: true,
  14. schema: [],
  15. messages: {
  16. unresolvablePath:
  17. 'The path "{{pathValue}}" in vi.doMock() cannot be resolved relative to the current file.',
  18. notAStringLiteral:
  19. 'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
  20. noArguments: 'vi.doMock() called with no arguments.',
  21. },
  22. },
  23. create(context) {
  24. const currentFilePath = context.getFilename()
  25. // ESLint can sometimes pass <text> or <input> for snippets not in a file
  26. if (currentFilePath === '<text>' || currentFilePath === '<input>') {
  27. return {}
  28. }
  29. const currentDirectory = path.dirname(currentFilePath)
  30. function canResolve(modulePath) {
  31. try {
  32. require.resolve(path.resolve(currentDirectory, modulePath))
  33. return true
  34. } catch (e) {
  35. const absolutePath = path.resolve(currentDirectory, modulePath)
  36. const extensions = [
  37. '',
  38. '.js',
  39. '.mjs',
  40. '.ts',
  41. '.jsx',
  42. '.tsx',
  43. '.json',
  44. '.node',
  45. '/index.js',
  46. '/index.ts',
  47. ] // Add common extensions
  48. for (const ext of extensions) {
  49. if (fs.existsSync(absolutePath + ext)) {
  50. return true
  51. }
  52. }
  53. return false
  54. }
  55. }
  56. return {
  57. CallExpression(node) {
  58. if (
  59. node.callee.type === 'MemberExpression' &&
  60. node.callee.object.type === 'Identifier' &&
  61. node.callee.object.name === 'vi' &&
  62. node.callee.property.type === 'Identifier' &&
  63. node.callee.property.name === 'doMock'
  64. ) {
  65. if (node.arguments.length === 0) {
  66. context.report({
  67. node,
  68. messageId: 'noArguments',
  69. })
  70. return
  71. }
  72. const firstArg = node.arguments[0]
  73. let pathValue = firstArg.value
  74. if (
  75. firstArg.type !== 'Literal' ||
  76. typeof firstArg.value !== 'string'
  77. ) {
  78. if (firstArg.type === 'Identifier') {
  79. const variable = context
  80. .getScope()
  81. .variables.find(v => v.name === firstArg.name)
  82. if (
  83. variable &&
  84. variable.defs.length > 0 &&
  85. variable.defs[0].node.init &&
  86. variable.defs[0].node.init.type === 'Literal' &&
  87. typeof variable.defs[0].node.init.value === 'string'
  88. ) {
  89. pathValue = variable.defs[0].node.init.value
  90. if (canResolve(pathValue)) {
  91. return
  92. }
  93. // If the first argument was a variable that didn't resolve then we can't auto-fix it
  94. }
  95. }
  96. context.report({
  97. node: firstArg,
  98. messageId: 'notAStringLiteral',
  99. })
  100. return
  101. }
  102. if (!pathValue.startsWith('.')) {
  103. return
  104. }
  105. if (!canResolve(pathValue)) {
  106. const mjsPath = pathValue.replace('.js', '.mjs')
  107. const additionalReportOptions = {}
  108. if (canResolve(mjsPath)) {
  109. additionalReportOptions.fix = fixer =>
  110. fixer.replaceText(firstArg, `'${mjsPath}'`)
  111. additionalReportOptions.suggest = [
  112. {
  113. desc: `Replace with "${pathValue.replace('.js', '.mjs')}"`,
  114. fix: fixer => fixer.replaceText(firstArg, `'${mjsPath}'`),
  115. },
  116. ]
  117. }
  118. context.report({
  119. node: firstArg,
  120. messageId: 'unresolvablePath',
  121. data: {
  122. pathValue,
  123. },
  124. ...additionalReportOptions,
  125. })
  126. }
  127. }
  128. },
  129. }
  130. },
  131. }