checked-requires-onchange-or-readonly.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /**
  2. * @fileoverview Enforce the use of the 'onChange' or 'readonly' attribute when 'checked' is used'
  3. * @author Jaesoekjjang
  4. */
  5. 'use strict';
  6. const ASTUtils = require('jsx-ast-utils');
  7. const flatMap = require('array.prototype.flatmap');
  8. const isCreateElement = require('../util/isCreateElement');
  9. const report = require('../util/report');
  10. const docsUrl = require('../util/docsUrl');
  11. const messages = {
  12. missingProperty: '`checked` should be used with either `onChange` or `readOnly`.',
  13. exclusiveCheckedAttribute: 'Use either `checked` or `defaultChecked`, but not both.',
  14. };
  15. const targetPropSet = new Set(['checked', 'onChange', 'readOnly', 'defaultChecked']);
  16. const defaultOptions = {
  17. ignoreMissingProperties: false,
  18. ignoreExclusiveCheckedAttribute: false,
  19. };
  20. /**
  21. * @param {object[]} properties
  22. * @param {string} keyName
  23. * @returns {Set<string>}
  24. */
  25. function extractTargetProps(properties, keyName) {
  26. return new Set(
  27. flatMap(
  28. properties,
  29. (prop) => (
  30. prop[keyName] && targetPropSet.has(prop[keyName].name)
  31. ? [prop[keyName].name]
  32. : []
  33. )
  34. )
  35. );
  36. }
  37. /** @type {import('eslint').Rule.RuleModule} */
  38. module.exports = {
  39. meta: {
  40. docs: {
  41. description: 'Enforce using `onChange` or `readonly` attribute when `checked` is used',
  42. category: 'Best Practices',
  43. recommended: false,
  44. url: docsUrl('checked-requires-onchange-or-readonly'),
  45. },
  46. messages,
  47. schema: [{
  48. additionalProperties: false,
  49. properties: {
  50. ignoreMissingProperties: {
  51. type: 'boolean',
  52. },
  53. ignoreExclusiveCheckedAttribute: {
  54. type: 'boolean',
  55. },
  56. },
  57. }],
  58. },
  59. create(context) {
  60. const options = Object.assign({}, defaultOptions, context.options[0]);
  61. function reportMissingProperty(node) {
  62. report(
  63. context,
  64. messages.missingProperty,
  65. 'missingProperty',
  66. { node }
  67. );
  68. }
  69. function reportExclusiveCheckedAttribute(node) {
  70. report(
  71. context,
  72. messages.exclusiveCheckedAttribute,
  73. 'exclusiveCheckedAttribute',
  74. { node }
  75. );
  76. }
  77. /**
  78. * @param {ASTNode} node
  79. * @param {Set<string>} propSet
  80. * @returns {void}
  81. */
  82. const checkAttributesAndReport = (node, propSet) => {
  83. if (!propSet.has('checked')) {
  84. return;
  85. }
  86. if (!options.ignoreExclusiveCheckedAttribute && propSet.has('defaultChecked')) {
  87. reportExclusiveCheckedAttribute(node);
  88. }
  89. if (
  90. !options.ignoreMissingProperties
  91. && !(propSet.has('onChange') || propSet.has('readOnly'))
  92. ) {
  93. reportMissingProperty(node);
  94. }
  95. };
  96. return {
  97. JSXOpeningElement(node) {
  98. if (ASTUtils.elementType(node) !== 'input') {
  99. return;
  100. }
  101. const propSet = extractTargetProps(node.attributes, 'name');
  102. checkAttributesAndReport(node, propSet);
  103. },
  104. CallExpression(node) {
  105. if (!isCreateElement(context, node)) {
  106. return;
  107. }
  108. const firstArg = node.arguments[0];
  109. const secondArg = node.arguments[1];
  110. if (
  111. !firstArg
  112. || firstArg.type !== 'Literal'
  113. || firstArg.value !== 'input'
  114. ) {
  115. return;
  116. }
  117. if (!secondArg || secondArg.type !== 'ObjectExpression') {
  118. return;
  119. }
  120. const propSet = extractTargetProps(secondArg.properties, 'key');
  121. checkAttributesAndReport(node, propSet);
  122. },
  123. };
  124. },
  125. };