Skip to content

Commit 42f9c0c

Browse files
committed
Merge pull request #392 from chrislaskey/required-first
Add requiredFirst option to jsx-sort-prop-types
2 parents 1371c76 + ce330d8 commit 42f9c0c

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

docs/rules/jsx-sort-prop-types.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ class Component extends React.Component {
8080
...
8181
"jsx-sort-prop-types": [<enabled>, {
8282
"callbacksLast": <boolean>,
83-
"ignoreCase": <boolean>
83+
"ignoreCase": <boolean>,
84+
"requiredFirst": <boolean>,
8485
}]
8586
...
8687
```
@@ -105,6 +106,22 @@ var Component = React.createClass({
105106
});
106107
```
107108

109+
### `requiredFirst`
110+
111+
When `true`, prop types for required props must be listed before all other props:
112+
113+
```js
114+
var Component = React.createClass({
115+
propTypes: {
116+
barRequired: React.PropTypes.any.isRequired,
117+
fooRequired: React.PropTypes.any.isRequired,
118+
a: React.PropTypes.number,
119+
z: React.PropTypes.string,
120+
},
121+
...
122+
});
123+
```
124+
108125
## When not to use
109126

110127
This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing props declarations isn't a part of your coding standards, then you can leave this rule off.

lib/rules/jsx-sort-prop-types.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
module.exports = function(context) {
1111

1212
var configuration = context.options[0] || {};
13+
var requiredFirst = configuration.requiredFirst || false;
1314
var callbacksLast = configuration.callbacksLast || false;
1415
var ignoreCase = configuration.ignoreCase || false;
1516

@@ -38,10 +39,18 @@ module.exports = function(context) {
3839
return node.key.type === 'Identifier' ? node.key.name : node.key.value;
3940
}
4041

42+
function getValueName(node) {
43+
return node.value.property.name;
44+
}
45+
4146
function isCallbackPropName(propName) {
4247
return /^on[A-Z]/.test(propName);
4348
}
4449

50+
function isRequiredProp(node) {
51+
return getValueName(node) === 'isRequired';
52+
}
53+
4554
/**
4655
* Checks if propTypes declarations are sorted
4756
* @param {Array} declarations The array of AST nodes being checked.
@@ -51,6 +60,8 @@ module.exports = function(context) {
5160
declarations.reduce(function(prev, curr) {
5261
var prevPropName = getKey(prev);
5362
var currentPropName = getKey(curr);
63+
var previousIsRequired = isRequiredProp(prev);
64+
var currentIsRequired = isRequiredProp(curr);
5465
var previousIsCallback = isCallbackPropName(prevPropName);
5566
var currentIsCallback = isCallbackPropName(currentPropName);
5667

@@ -59,6 +70,18 @@ module.exports = function(context) {
5970
currentPropName = currentPropName.toLowerCase();
6071
}
6172

73+
if (requiredFirst) {
74+
if (previousIsRequired && !currentIsRequired) {
75+
// Transition between required and non-required. Don't compare for alphabetical.
76+
return curr;
77+
}
78+
if (!previousIsRequired && currentIsRequired) {
79+
// Encountered a non-required prop after a required prop
80+
context.report(curr, 'Required prop types must be listed before all other prop types');
81+
return curr;
82+
}
83+
}
84+
6285
if (callbacksLast) {
6386
if (!previousIsCallback && currentIsCallback) {
6487
// Entering the callback prop section
@@ -117,6 +140,9 @@ module.exports = function(context) {
117140
module.exports.schema = [{
118141
type: 'object',
119142
properties: {
143+
requiredFirst: {
144+
type: 'boolean'
145+
},
120146
callbacksLast: {
121147
type: 'boolean'
122148
},

tests/lib/rules/jsx-sort-prop-types.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,20 @@ ruleTester.run('jsx-sort-prop-types', rule, {
191191
'});'
192192
].join('\n'),
193193
parserOptions: parserOptions
194+
}, {
195+
code: [
196+
'var First = React.createClass({',
197+
' propTypes: {',
198+
' barRequired: React.PropTypes.func.isRequired,',
199+
' onBar: React.PropTypes.func,',
200+
' z: React.PropTypes.any',
201+
' },',
202+
' render: function() {',
203+
' return <div />;',
204+
' }',
205+
'});'
206+
].join('\n'),
207+
parserOptions: parserOptions
194208
}, {
195209
code: [
196210
'var First = React.createClass({',
@@ -246,6 +260,43 @@ ruleTester.run('jsx-sort-prop-types', rule, {
246260
callbacksLast: true
247261
}],
248262
parserOptions: parserOptions
263+
}, {
264+
code: [
265+
'class First extends React.Component {',
266+
' render() {',
267+
' return <div />;',
268+
' }',
269+
'}',
270+
'First.propTypes = {',
271+
' barRequired: React.PropTypes.string.isRequired,',
272+
' a: React.PropTypes.any',
273+
'};'
274+
].join('\n'),
275+
options: [{
276+
requiredFirst: true
277+
}],
278+
parserOptions: parserOptions
279+
}, {
280+
code: [
281+
'class First extends React.Component {',
282+
' render() {',
283+
' return <div />;',
284+
' }',
285+
'}',
286+
'First.propTypes = {',
287+
' barRequired: React.PropTypes.string.isRequired,',
288+
' fooRequired: React.PropTypes.any.isRequired,',
289+
' a: React.PropTypes.any,',
290+
' z: React.PropTypes.string,',
291+
' onBar: React.PropTypes.func,',
292+
' onFoo: React.PropTypes.func',
293+
'};'
294+
].join('\n'),
295+
options: [{
296+
requiredFirst: true,
297+
callbacksLast: true
298+
}],
299+
parserOptions: parserOptions
249300
}],
250301

251302
invalid: [{
@@ -483,5 +534,51 @@ ruleTester.run('jsx-sort-prop-types', rule, {
483534
column: 5,
484535
type: 'Property'
485536
}]
537+
}, {
538+
code: [
539+
'var First = React.createClass({',
540+
' propTypes: {',
541+
' fooRequired: React.PropTypes.string.isRequired,',
542+
' barRequired: React.PropTypes.string.isRequired,',
543+
' a: React.PropTypes.any',
544+
' },',
545+
' render: function() {',
546+
' return <div />;',
547+
' }',
548+
'});'
549+
].join('\n'),
550+
options: [{
551+
requiredFirst: true
552+
}],
553+
parserOptions: parserOptions,
554+
errors: [{
555+
message: ERROR_MESSAGE,
556+
line: 4,
557+
column: 5,
558+
type: 'Property'
559+
}]
560+
}, {
561+
code: [
562+
'var First = React.createClass({',
563+
' propTypes: {',
564+
' a: React.PropTypes.any,',
565+
' barRequired: React.PropTypes.string.isRequired,',
566+
' onFoo: React.PropTypes.func',
567+
' },',
568+
' render: function() {',
569+
' return <div />;',
570+
' }',
571+
'});'
572+
].join('\n'),
573+
options: [{
574+
requiredFirst: true
575+
}],
576+
parserOptions: parserOptions,
577+
errors: [{
578+
message: 'Required prop types must be listed before all other prop types',
579+
line: 4,
580+
column: 5,
581+
type: 'Property'
582+
}]
486583
}]
487584
});

0 commit comments

Comments
 (0)