no-typos.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. const path = require('path')
  2. const NEXT_EXPORT_FUNCTIONS = [
  3. 'getStaticProps',
  4. 'getStaticPaths',
  5. 'getServerSideProps',
  6. ]
  7. // 0 is the exact match
  8. const THRESHOLD = 1
  9. // the minimum number of operations required to convert string a to string b.
  10. function minDistance(a, b) {
  11. const m = a.length
  12. const n = b.length
  13. if (m < n) {
  14. return minDistance(b, a)
  15. }
  16. if (n === 0) {
  17. return m
  18. }
  19. let previousRow = Array.from({ length: n + 1 }, (_, i) => i)
  20. for (let i = 0; i < m; i++) {
  21. const s1 = a[i]
  22. let currentRow = [i + 1]
  23. for (let j = 0; j < n; j++) {
  24. const s2 = b[j]
  25. const insertions = previousRow[j + 1] + 1
  26. const deletions = currentRow[j] + 1
  27. const substitutions = previousRow[j] + Number(s1 !== s2)
  28. currentRow.push(Math.min(insertions, deletions, substitutions))
  29. }
  30. previousRow = currentRow
  31. }
  32. return previousRow[previousRow.length - 1]
  33. }
  34. /* eslint-disable eslint-plugin/require-meta-docs-url */
  35. module.exports = {
  36. meta: {
  37. docs: {
  38. description: 'Prevent common typos in Next.js data fetching functions.',
  39. recommended: true,
  40. },
  41. type: 'problem',
  42. schema: [],
  43. },
  44. create: function (context) {
  45. function checkTypos(node, name) {
  46. if (NEXT_EXPORT_FUNCTIONS.includes(name)) {
  47. return
  48. }
  49. const potentialTypos = NEXT_EXPORT_FUNCTIONS.map((o) => ({
  50. option: o,
  51. distance: minDistance(o, name),
  52. }))
  53. .filter(({ distance }) => distance <= THRESHOLD && distance > 0)
  54. .sort((a, b) => a.distance - b.distance)
  55. if (potentialTypos.length) {
  56. context.report({
  57. node,
  58. message: `${name} may be a typo. Did you mean ${potentialTypos[0].option}?`,
  59. })
  60. }
  61. }
  62. return {
  63. ExportNamedDeclaration(node) {
  64. const page = context.getFilename().split('pages')[1]
  65. if (!page || path.parse(page).dir.startsWith('/api')) {
  66. return
  67. }
  68. const decl = node.declaration
  69. if (!decl) {
  70. return
  71. }
  72. switch (decl.type) {
  73. case 'FunctionDeclaration': {
  74. checkTypos(node, decl.id.name)
  75. break
  76. }
  77. case 'VariableDeclaration': {
  78. decl.declarations.forEach((d) => {
  79. if (d.id.type !== 'Identifier') {
  80. return
  81. }
  82. checkTypos(node, d.id.name)
  83. })
  84. break
  85. }
  86. default: {
  87. break
  88. }
  89. }
  90. return
  91. },
  92. }
  93. },
  94. }