catch-error-name.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. 'use strict';
  2. const {findVariable} = require('@eslint-community/eslint-utils');
  3. const avoidCapture = require('./utils/avoid-capture.js');
  4. const {renameVariable} = require('./fix/index.js');
  5. const {matches, methodCallSelector} = require('./selectors/index.js');
  6. const MESSAGE_ID = 'catch-error-name';
  7. const messages = {
  8. [MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.',
  9. };
  10. const selector = matches([
  11. // `try {} catch (foo) {}`
  12. [
  13. 'CatchClause',
  14. ' > ',
  15. 'Identifier.param',
  16. ].join(''),
  17. // - `promise.then(…, foo => {})`
  18. // - `promise.then(…, function(foo) {})`
  19. // - `promise.catch(foo => {})`
  20. // - `promise.catch(function(foo) {})`
  21. [
  22. matches([
  23. methodCallSelector({method: 'then', argumentsLength: 2}),
  24. methodCallSelector({method: 'catch', argumentsLength: 1}),
  25. ]),
  26. ' > ',
  27. ':matches(FunctionExpression, ArrowFunctionExpression).arguments:last-child',
  28. ' > ',
  29. 'Identifier.params:first-child',
  30. ].join(''),
  31. ]);
  32. /** @param {import('eslint').Rule.RuleContext} context */
  33. const create = context => {
  34. const options = {
  35. name: 'error',
  36. ignore: [],
  37. ...context.options[0],
  38. };
  39. const {name: expectedName} = options;
  40. const ignore = options.ignore.map(
  41. pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'),
  42. );
  43. const isNameAllowed = name =>
  44. name === expectedName
  45. || ignore.some(regexp => regexp.test(name))
  46. || name.endsWith(expectedName)
  47. || name.endsWith(expectedName.charAt(0).toUpperCase() + expectedName.slice(1));
  48. return {
  49. [selector](node) {
  50. const originalName = node.name;
  51. if (
  52. isNameAllowed(originalName)
  53. || isNameAllowed(originalName.replace(/_+$/g, ''))
  54. ) {
  55. return;
  56. }
  57. const scope = context.getScope();
  58. const variable = findVariable(scope, node);
  59. // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768072967
  60. // But can't reproduce, just ignore this case
  61. /* c8 ignore next 3 */
  62. if (!variable) {
  63. return;
  64. }
  65. if (originalName === '_' && variable.references.length === 0) {
  66. return;
  67. }
  68. const scopes = [
  69. variable.scope,
  70. ...variable.references.map(({from}) => from),
  71. ];
  72. const fixedName = avoidCapture(expectedName, scopes);
  73. const problem = {
  74. node,
  75. messageId: MESSAGE_ID,
  76. data: {
  77. originalName,
  78. fixedName: fixedName || expectedName,
  79. },
  80. };
  81. if (fixedName) {
  82. problem.fix = fixer => renameVariable(variable, fixedName, fixer);
  83. }
  84. return problem;
  85. },
  86. };
  87. };
  88. const schema = [
  89. {
  90. type: 'object',
  91. additionalProperties: false,
  92. properties: {
  93. name: {
  94. type: 'string',
  95. },
  96. ignore: {
  97. type: 'array',
  98. uniqueItems: true,
  99. },
  100. },
  101. },
  102. ];
  103. /** @type {import('eslint').Rule.RuleModule} */
  104. module.exports = {
  105. create,
  106. meta: {
  107. type: 'suggestion',
  108. docs: {
  109. description: 'Enforce a specific parameter name in catch clauses.',
  110. },
  111. fixable: 'code',
  112. schema,
  113. messages,
  114. },
  115. };