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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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.filename
  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 scope = context.sourceCode.getScope(node)
  80. const variable = scope.variables.find(
  81. v => v.name === firstArg.name
  82. )
  83. if (
  84. variable &&
  85. variable.defs.length > 0 &&
  86. variable.defs[0].node.init &&
  87. variable.defs[0].node.init.type === 'Literal' &&
  88. typeof variable.defs[0].node.init.value === 'string'
  89. ) {
  90. pathValue = variable.defs[0].node.init.value
  91. if (canResolve(pathValue)) {
  92. return
  93. }
  94. // If the first argument was a variable that didn't resolve then we can't auto-fix it
  95. }
  96. }
  97. context.report({
  98. node: firstArg,
  99. messageId: 'notAStringLiteral',
  100. })
  101. return
  102. }
  103. if (!pathValue.startsWith('.')) {
  104. return
  105. }
  106. if (!canResolve(pathValue)) {
  107. const mjsPath = pathValue.replace('.js', '.mjs')
  108. const additionalReportOptions = {}
  109. if (canResolve(mjsPath)) {
  110. additionalReportOptions.fix = fixer =>
  111. fixer.replaceText(firstArg, `'${mjsPath}'`)
  112. additionalReportOptions.suggest = [
  113. {
  114. desc: `Replace with "${pathValue.replace('.js', '.mjs')}"`,
  115. fix: fixer => fixer.replaceText(firstArg, `'${mjsPath}'`),
  116. },
  117. ]
  118. }
  119. context.report({
  120. node: firstArg,
  121. messageId: 'unresolvablePath',
  122. data: {
  123. pathValue,
  124. },
  125. ...additionalReportOptions,
  126. })
  127. }
  128. }
  129. },
  130. }
  131. },
  132. }