self-closing-comp.js 2.9 KB

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