Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 381c7c1

Browse files
jaesoekjjangljharb
authored andcommittedJan 20, 2024
[New] add checked-requires-onchange-or-readonly
1 parent 4edc1f0 commit 381c7c1

File tree

5 files changed

+399
-102
lines changed

5 files changed

+399
-102
lines changed
 

‎README.md

Lines changed: 103 additions & 102 deletions
Large diffs are not rendered by default.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Enforce using `onChange` or `readonly` attribute when `checked` is used (`react/checked-requires-onchange-or-readonly`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
This rule enforces `onChange` or `readonly` attribute for `checked` property of input elements.
6+
7+
It also warns when `checked` and `defaultChecked` properties are used together.
8+
9+
## Rule Details
10+
11+
Example of **incorrect** code for this rule:
12+
13+
```jsx
14+
<input type="checkbox" checked />
15+
<input type="checkbox" checked defaultChecked />
16+
<input type="radio" checked defaultChecked />
17+
18+
React.createElement('input', { checked: false });
19+
React.createElement('input', { type: 'checkbox', checked: true });
20+
React.createElement('input', { type: 'checkbox', checked: true, defaultChecked: true });
21+
```
22+
23+
Example of **correct** code for this rule:
24+
25+
```jsx
26+
<input type="checkbox" checked onChange={() => {}} />
27+
<input type="checkbox" checked readOnly />
28+
<input type="checkbox" checked onChange readOnly />
29+
<input type="checkbox" defaultChecked />
30+
31+
React.createElement('input', { type: 'checkbox', checked: true, onChange() {} });
32+
React.createElement('input', { type: 'checkbox', checked: true, readOnly: true });
33+
React.createElement('input', { type: 'checkbox', checked: true, onChange() {}, readOnly: true });
34+
React.createElement('input', { type: 'checkbox', defaultChecked: true });
35+
```
36+
37+
## Rule Options
38+
39+
```js
40+
"react/checked-requires-onchange-or-readonly": [<enabled>, {
41+
"ignoreMissingProperties": <boolean>,
42+
"ignoreExclusiveCheckedAttribute": <boolean>
43+
}]
44+
```
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @fileoverview Enforce the use of the 'onChange' or 'readonly' attribute when 'checked' is used'
3+
* @author Jaesoekjjang
4+
*/
5+
6+
'use strict';
7+
8+
const ASTUtils = require('jsx-ast-utils');
9+
const flatMap = require('array.prototype.flatmap');
10+
const isCreateElement = require('../util/isCreateElement');
11+
const report = require('../util/report');
12+
const docsUrl = require('../util/docsUrl');
13+
14+
const messages = {
15+
missingProperty: '`checked` should be used with either `onChange` or `readOnly`.',
16+
exclusiveCheckedAttribute: 'Use either `checked` or `defaultChecked`, but not both.',
17+
};
18+
19+
const targetPropSet = new Set(['checked', 'onChange', 'readOnly', 'defaultChecked']);
20+
21+
const defaultOptions = {
22+
ignoreMissingProperties: true,
23+
ignoreExclusiveCheckedAttribute: true,
24+
};
25+
26+
/**
27+
* @param {string[]} properties
28+
* @param {string} keyName
29+
* @returns {Set<string>}
30+
*/
31+
function extractTargetProps(properties, keyName) {
32+
return new Set(
33+
flatMap(
34+
properties,
35+
(prop) => (
36+
prop[keyName] && targetPropSet.has(prop[keyName].name)
37+
? [prop[keyName].name]
38+
: []
39+
)
40+
)
41+
);
42+
}
43+
44+
module.exports = {
45+
meta: {
46+
docs: {
47+
description: 'Enforce using `onChange` or `readonly` attribute when `checked` is used',
48+
category: 'Best Practices',
49+
recommended: false,
50+
url: docsUrl('checked-requires-onchange-or-readonly'),
51+
},
52+
messages,
53+
schema: [{
54+
additionalProperties: false,
55+
properties: {
56+
ignoreMissingProperties: {
57+
type: 'boolean',
58+
},
59+
ignoreExclusiveCheckedAttribute: {
60+
type: 'boolean',
61+
},
62+
},
63+
}],
64+
},
65+
create(context) {
66+
const options = Object.assign({}, defaultOptions, context.options[0]);
67+
68+
function reportMissingProperty(node) {
69+
report(
70+
context,
71+
messages.missingProperty,
72+
'missingProperty',
73+
{ node }
74+
);
75+
}
76+
77+
function reportExclusiveCheckedAttribute(node) {
78+
report(
79+
context,
80+
messages.exclusiveCheckedAttribute,
81+
'exclusiveCheckedAttribute',
82+
{ node }
83+
);
84+
}
85+
86+
/**
87+
* @param {ASTNode} node
88+
* @param {Set<string>} propSet
89+
* @returns {void}
90+
*/
91+
const checkAttributesAndReport = (node, propSet) => {
92+
if (!propSet.has('checked')) {
93+
return;
94+
}
95+
96+
if (options.ignoreExclusiveCheckedAttribute && propSet.has('defaultChecked')) {
97+
reportExclusiveCheckedAttribute(node);
98+
}
99+
100+
if (
101+
options.ignoreMissingProperties
102+
&& !(propSet.has('onChange') || propSet.has('readOnly'))
103+
) {
104+
reportMissingProperty(node);
105+
}
106+
};
107+
108+
return {
109+
JSXOpeningElement(node) {
110+
if (ASTUtils.elementType(node) !== 'input') {
111+
return;
112+
}
113+
114+
const propSet = extractTargetProps(node.attributes, 'name');
115+
checkAttributesAndReport(node, propSet);
116+
},
117+
CallExpression(node) {
118+
if (!isCreateElement(node, context)) {
119+
return;
120+
}
121+
122+
const [firstArg, secondArg] = node.arguments;
123+
if (
124+
!firstArg
125+
|| firstArg.type !== 'Literal'
126+
|| firstArg.value !== 'input'
127+
) {
128+
return;
129+
}
130+
131+
if (!secondArg || secondArg.type !== 'ObjectExpression') {
132+
return;
133+
}
134+
135+
const propSet = extractTargetProps(secondArg.properties, 'key');
136+
checkAttributesAndReport(node, propSet);
137+
},
138+
};
139+
},
140+
};

‎lib/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module.exports = {
66
'boolean-prop-naming': require('./boolean-prop-naming'),
77
'button-has-type': require('./button-has-type'),
8+
'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'),
89
'default-props-match-prop-types': require('./default-props-match-prop-types'),
910
'destructuring-assignment': require('./destructuring-assignment'),
1011
'display-name': require('./display-name'),
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* @fileoverview Enforce the use of the 'onChange' or 'readonly' attribute when 'checked' is used'
3+
* @author Jaesoekjjang
4+
*/
5+
6+
'use strict';
7+
8+
const RuleTester = require('eslint').RuleTester;
9+
const rule = require('../../../lib/rules/checked-requires-onchange-or-readonly');
10+
11+
const parsers = require('../../helpers/parsers');
12+
13+
const ruleTester = new RuleTester({
14+
parserOptions: {
15+
ecmaVersion: 2018,
16+
sourceType: 'module',
17+
ecmaFeatures: {
18+
jsx: true,
19+
},
20+
},
21+
});
22+
23+
ruleTester.run('checked-requires-onchange-or-readonly', rule, {
24+
valid: parsers.all([
25+
'<input type="checkbox" />',
26+
'<input type="checkbox" onChange={noop} />',
27+
'<input type="checkbox" readOnly />',
28+
'<input type="checkbox" checked onChange={noop} />',
29+
'<input type="checkbox" checked={true} onChange={noop} />',
30+
'<input type="checkbox" checked={false} onChange={noop} />',
31+
'<input type="checkbox" checked readOnly />',
32+
'<input type="checkbox" checked={true} readOnly />',
33+
'<input type="checkbox" checked={false} readOnly />',
34+
'<input type="checkbox" defaultChecked />',
35+
"React.createElement('input')",
36+
"React.createElement('input', { checked: true, onChange: noop })",
37+
"React.createElement('input', { checked: false, onChange: noop })",
38+
"React.createElement('input', { checked: true, readOnly: true })",
39+
"React.createElement('input', { checked: true, onChange: noop, readOnly: true })",
40+
"React.createElement('input', { checked: foo, onChange: noop, readOnly: true })",
41+
{
42+
code: '<input type="checkbox" checked />',
43+
options: [{ ignoreMissingProperties: false }],
44+
},
45+
{
46+
code: '<input type="checkbox" checked={true} />',
47+
options: [{ ignoreMissingProperties: false }],
48+
},
49+
{
50+
code: '<input type="checkbox" onChange={noop} checked defaultChecked />',
51+
options: [{ ignoreExclusiveCheckedAttribute: false }],
52+
},
53+
{
54+
code: '<input type="checkbox" onChange={noop} checked={true} defaultChecked />',
55+
options: [{ ignoreExclusiveCheckedAttribute: false }],
56+
},
57+
'<span/>',
58+
"React.createElement('span')",
59+
'(()=>{})()',
60+
]),
61+
invalid: parsers.all([
62+
{
63+
code: '<input type="radio" checked />',
64+
errors: [{ messageId: 'missingProperty' }],
65+
},
66+
{
67+
code: '<input type="radio" checked={true} />',
68+
errors: [{ messageId: 'missingProperty' }],
69+
},
70+
{
71+
code: '<input type="checkbox" checked />',
72+
errors: [{ messageId: 'missingProperty' }],
73+
},
74+
{
75+
code: '<input type="checkbox" checked={true} />',
76+
errors: [{ messageId: 'missingProperty' }],
77+
},
78+
{
79+
code: '<input type="checkbox" checked={condition ? true : false} />',
80+
errors: [{ messageId: 'missingProperty' }],
81+
},
82+
{
83+
code: '<input type="checkbox" checked defaultChecked />',
84+
errors: [
85+
{ messageId: 'exclusiveCheckedAttribute' },
86+
{ messageId: 'missingProperty' },
87+
],
88+
},
89+
{
90+
code: 'React.createElement("input", { checked: false })',
91+
errors: [{ messageId: 'missingProperty' }],
92+
},
93+
{
94+
code: 'React.createElement("input", { checked: true, defaultChecked: true })',
95+
errors: [
96+
{ messageId: 'exclusiveCheckedAttribute' },
97+
{ messageId: 'missingProperty' },
98+
],
99+
},
100+
{
101+
code: '<input type="checkbox" checked defaultChecked />',
102+
options: [{ ignoreMissingProperties: false }],
103+
errors: [{ messageId: 'exclusiveCheckedAttribute' }],
104+
},
105+
{
106+
code: '<input type="checkbox" checked defaultChecked />',
107+
options: [{ ignoreExclusiveCheckedAttribute: false }],
108+
errors: [{ messageId: 'missingProperty' }],
109+
},
110+
]),
111+
});

0 commit comments

Comments
 (0)
Please sign in to comment.