custom-error-definition.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use strict';
  2. const {upperFirst} = require('lodash');
  3. const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
  4. const messages = {
  5. [MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class',
  6. };
  7. const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
  8. const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
  9. const getConstructorMethod = className => `
  10. constructor() {
  11. super();
  12. this.name = '${className}';
  13. }
  14. `;
  15. const hasValidSuperClass = node => {
  16. if (!node.superClass) {
  17. return false;
  18. }
  19. let {name, type, property} = node.superClass;
  20. if (type === 'MemberExpression') {
  21. ({name} = property);
  22. }
  23. return nameRegexp.test(name);
  24. };
  25. const isSuperExpression = node =>
  26. node.type === 'ExpressionStatement'
  27. && node.expression.type === 'CallExpression'
  28. && node.expression.callee.type === 'Super';
  29. const isAssignmentExpression = (node, name) => {
  30. if (
  31. node.type !== 'ExpressionStatement'
  32. || node.expression.type !== 'AssignmentExpression'
  33. ) {
  34. return false;
  35. }
  36. const lhs = node.expression.left;
  37. if (!lhs.object || lhs.object.type !== 'ThisExpression') {
  38. return false;
  39. }
  40. return lhs.property.name === name;
  41. };
  42. const isPropertyDefinition = (node, name) =>
  43. node.type === 'PropertyDefinition'
  44. && !node.computed
  45. && node.key.type === 'Identifier'
  46. && node.key.name === name;
  47. function * customErrorDefinition(context, node) {
  48. if (!hasValidSuperClass(node)) {
  49. return;
  50. }
  51. if (node.id === null) {
  52. return;
  53. }
  54. const {name} = node.id;
  55. const className = getClassName(name);
  56. if (name !== className) {
  57. yield {
  58. node: node.id,
  59. message: `Invalid class name, use \`${className}\`.`,
  60. };
  61. }
  62. const {body, range} = node.body;
  63. const constructor = body.find(x => x.kind === 'constructor');
  64. if (!constructor) {
  65. yield {
  66. node,
  67. message: 'Add a constructor to your error.',
  68. fix: fixer => fixer.insertTextAfterRange([
  69. range[0],
  70. range[0] + 1,
  71. ], getConstructorMethod(name)),
  72. };
  73. return;
  74. }
  75. const constructorBodyNode = constructor.value.body;
  76. // Verify the constructor has a body (TypeScript)
  77. if (!constructorBodyNode) {
  78. return;
  79. }
  80. const constructorBody = constructorBodyNode.body;
  81. const superExpression = constructorBody.find(body => isSuperExpression(body));
  82. const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, 'message'));
  83. if (!superExpression) {
  84. yield {
  85. node: constructorBodyNode,
  86. message: 'Missing call to `super()` in constructor.',
  87. };
  88. } else if (messageExpressionIndex !== -1) {
  89. const expression = constructorBody[messageExpressionIndex];
  90. yield {
  91. node: superExpression,
  92. message: 'Pass the error message to `super()` instead of setting `this.message`.',
  93. * fix(fixer) {
  94. if (superExpression.expression.arguments.length === 0) {
  95. const rhs = expression.expression.right;
  96. yield fixer.insertTextAfterRange([
  97. superExpression.range[0],
  98. superExpression.range[0] + 6,
  99. ], rhs.raw || rhs.name);
  100. }
  101. yield fixer.removeRange([
  102. messageExpressionIndex === 0 ? constructorBodyNode.range[0] : constructorBody[messageExpressionIndex - 1].range[1],
  103. expression.range[1],
  104. ]);
  105. },
  106. };
  107. }
  108. const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
  109. if (!nameExpression) {
  110. const nameProperty = body.find(node => isPropertyDefinition(node, 'name'));
  111. if (!nameProperty?.value || nameProperty.value.value !== name) {
  112. yield {
  113. node: nameProperty?.value ?? constructorBodyNode,
  114. message: `The \`name\` property should be set to \`${name}\`.`,
  115. };
  116. }
  117. } else if (nameExpression.expression.right.value !== name) {
  118. yield {
  119. node: nameExpression?.expression.right ?? constructorBodyNode,
  120. message: `The \`name\` property should be set to \`${name}\`.`,
  121. };
  122. }
  123. }
  124. const customErrorExport = (context, node) => {
  125. const exportsName = node.left.property.name;
  126. const maybeError = node.right;
  127. if (maybeError.type !== 'ClassExpression') {
  128. return;
  129. }
  130. if (!hasValidSuperClass(maybeError)) {
  131. return;
  132. }
  133. if (!maybeError.id) {
  134. return;
  135. }
  136. // Assume rule has already fixed the error name
  137. const errorName = maybeError.id.name;
  138. if (exportsName === errorName) {
  139. return;
  140. }
  141. return {
  142. node: node.left.property,
  143. messageId: MESSAGE_ID_INVALID_EXPORT,
  144. fix: fixer => fixer.replaceText(node.left.property, errorName),
  145. };
  146. };
  147. /** @param {import('eslint').Rule.RuleContext} context */
  148. const create = context => ({
  149. ClassDeclaration: node => customErrorDefinition(context, node),
  150. 'AssignmentExpression[right.type="ClassExpression"]': node => customErrorDefinition(context, node.right),
  151. 'AssignmentExpression[left.type="MemberExpression"][left.object.type="Identifier"][left.object.name="exports"]': node => customErrorExport(context, node),
  152. });
  153. /** @type {import('eslint').Rule.RuleModule} */
  154. module.exports = {
  155. create,
  156. meta: {
  157. type: 'problem',
  158. docs: {
  159. description: 'Enforce correct `Error` subclassing.',
  160. },
  161. fixable: 'code',
  162. messages,
  163. },
  164. };