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

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