self-closing-comp.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /**
  2. * @fileoverview Prevent extra closing tags for components without children
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const jsxUtil = require('../util/jsx');
  8. const report = require('../util/report');
  9. const optionDefaults = { component: true, html: true };
  10. function isComponent(node) {
  11. return (
  12. node.name
  13. && (node.name.type === 'JSXIdentifier' || node.name.type === 'JSXMemberExpression')
  14. && !jsxUtil.isDOMComponent(node)
  15. );
  16. }
  17. function childrenIsEmpty(node) {
  18. return node.parent.children.length === 0;
  19. }
  20. function childrenIsMultilineSpaces(node) {
  21. const childrens = node.parent.children;
  22. return (
  23. childrens.length === 1
  24. && (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText')
  25. && childrens[0].value.indexOf('\n') !== -1
  26. && childrens[0].value.replace(/(?!\xA0)\s/g, '') === ''
  27. );
  28. }
  29. // ------------------------------------------------------------------------------
  30. // Rule Definition
  31. // ------------------------------------------------------------------------------
  32. const messages = {
  33. notSelfClosing: 'Empty components are self-closing',
  34. };
  35. /** @type {import('eslint').Rule.RuleModule} */
  36. module.exports = {
  37. meta: {
  38. docs: {
  39. description: 'Disallow extra closing tags for components without children',
  40. category: 'Stylistic Issues',
  41. recommended: false,
  42. url: docsUrl('self-closing-comp'),
  43. },
  44. fixable: 'code',
  45. messages,
  46. schema: [{
  47. type: 'object',
  48. properties: {
  49. component: {
  50. default: optionDefaults.component,
  51. type: 'boolean',
  52. },
  53. html: {
  54. default: optionDefaults.html,
  55. type: 'boolean',
  56. },
  57. },
  58. additionalProperties: false,
  59. }],
  60. },
  61. create(context) {
  62. function isShouldBeSelfClosed(node) {
  63. const configuration = Object.assign({}, optionDefaults, context.options[0]);
  64. return (
  65. (configuration.component && isComponent(node))
  66. || (configuration.html && jsxUtil.isDOMComponent(node))
  67. ) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node));
  68. }
  69. return {
  70. JSXOpeningElement(node) {
  71. if (!isShouldBeSelfClosed(node)) {
  72. return;
  73. }
  74. report(context, messages.notSelfClosing, 'notSelfClosing', {
  75. node,
  76. fix(fixer) {
  77. // Represents the last character of the JSXOpeningElement, the '>' character
  78. const openingElementEnding = node.range[1] - 1;
  79. // Represents the last character of the JSXClosingElement, the '>' character
  80. const closingElementEnding = node.parent.closingElement.range[1];
  81. // Replace />.*<\/.*>/ with '/>'
  82. const range = [openingElementEnding, closingElementEnding];
  83. return fixer.replaceTextRange(range, ' />');
  84. },
  85. });
  86. },
  87. };
  88. },
  89. };