self-closing-comp.js 3.0 KB

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