Skip to content

Commit 73d125f

Browse files
committed
Update jsx-sort-prop-types to allow sorting callbacks last
1 parent 719764b commit 73d125f

File tree

5 files changed

+220
-6
lines changed

5 files changed

+220
-6
lines changed

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,33 @@ class Component extends React.Component {
7878

7979
```js
8080
...
81-
"jsx-sort-prop-types": [<enabled>, { "ignoreCase": <boolean> }]
81+
"jsx-sort-prop-types": [<enabled>, {
82+
"callbacksLast": <boolean>,
83+
"ignoreCase": <boolean>
84+
}]
8285
...
8386
```
8487

8588
### `ignoreCase`
8689

8790
When `true` the rule ignores the case-sensitivity of the declarations order.
8891

92+
### `callbacksLast`
93+
94+
When `true`, prop types for props beginning with "on" must be listed after all other props:
95+
96+
```js
97+
var Component = React.createClass({
98+
propTypes: {
99+
a: React.PropTypes.number,
100+
z: React.PropTypes.string,
101+
onBar: React.PropTypes.func,
102+
onFoo: React.PropTypes.func,
103+
},
104+
...
105+
});
106+
```
107+
89108
## When not to use
90109

91110
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.

docs/rules/jsx-sort-props.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ The following patterns are considered okay and do not cause warnings:
2424

2525
```js
2626
...
27-
"jsx-sort-props": [<enabled>, { "ignoreCase": <boolean> }]
27+
"jsx-sort-props": [<enabled>, {
28+
"callbacksLast": <boolean>,
29+
"ignoreCase": <boolean>
30+
}]
2831
...
2932
```
3033

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

Lines changed: 25 additions & 3 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 callbacksLast = configuration.callbacksLast || false;
1314
var ignoreCase = configuration.ignoreCase || false;
1415

1516
/**
@@ -39,6 +40,10 @@ module.exports = function(context) {
3940
return node.key.type === 'Identifier' ? node.key.name : node.key.value;
4041
}
4142

43+
function isCallbackPropName(propName) {
44+
return /^on[A-Z]/.test(propName);
45+
}
46+
4247
/**
4348
* Checks if propTypes declarations are sorted
4449
* @param {Array} declarations The array of AST nodes being checked.
@@ -47,14 +52,28 @@ module.exports = function(context) {
4752
function checkSorted(declarations) {
4853
declarations.reduce(function(prev, curr) {
4954
var prevPropName = getKey(prev);
50-
var currenPropName = getKey(curr);
55+
var currentPropName = getKey(curr);
56+
var previousIsCallback = isCallbackPropName(prevPropName);
57+
var currentIsCallback = isCallbackPropName(currentPropName);
5158

5259
if (ignoreCase) {
5360
prevPropName = prevPropName.toLowerCase();
54-
currenPropName = currenPropName.toLowerCase();
61+
currentPropName = currentPropName.toLowerCase();
62+
}
63+
64+
if (callbacksLast) {
65+
if (!previousIsCallback && currentIsCallback) {
66+
// Entering the callback prop section
67+
return curr;
68+
}
69+
if (previousIsCallback && !currentIsCallback) {
70+
// Encountered a non-callback prop after a callback prop
71+
context.report(prev, 'Callback prop types must be listed after all other prop types');
72+
return prev;
73+
}
5574
}
5675

57-
if (currenPropName < prevPropName) {
76+
if (currentPropName < prevPropName) {
5877
context.report(curr, 'Prop types declarations should be sorted alphabetically');
5978
return prev;
6079
}
@@ -100,6 +119,9 @@ module.exports = function(context) {
100119
module.exports.schema = [{
101120
type: 'object',
102121
properties: {
122+
callbacksLast: {
123+
type: 'boolean'
124+
},
103125
ignoreCase: {
104126
type: 'boolean'
105127
}

lib/rules/jsx-sort-props.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module.exports = function(context) {
4242
}
4343
if (previousIsCallback && !currentIsCallback) {
4444
// Encountered a non-callback prop after a callback prop
45-
context.report(decl, 'Callbacks must be listed after all other props');
45+
context.report(memo, 'Callbacks must be listed after all other props');
4646
return memo;
4747
}
4848
}

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

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,69 @@ ruleTester.run('jsx-sort-prop-types', rule, {
208208
experimentalObjectRestSpread: true,
209209
jsx: true
210210
}
211+
}, {
212+
code: [
213+
'var First = React.createClass({',
214+
' propTypes: {',
215+
' a: React.PropTypes.any,',
216+
' z: React.PropTypes.string,',
217+
' onBar: React.PropTypes.func,',
218+
' onFoo: React.PropTypes.func',
219+
' },',
220+
' render: function() {',
221+
' return <div />;',
222+
' }',
223+
'});'
224+
].join('\n'),
225+
options: [{
226+
callbacksLast: true
227+
}],
228+
ecmaFeatures: {
229+
jsx: true
230+
}
231+
}, {
232+
code: [
233+
'class Component extends React.Component {',
234+
' static propTypes = {',
235+
' a: React.PropTypes.any,',
236+
' z: React.PropTypes.string,',
237+
' onBar: React.PropTypes.func,',
238+
' onFoo: React.PropTypes.func',
239+
' }',
240+
' render() {',
241+
' return <div />;',
242+
' }',
243+
'}'
244+
].join('\n'),
245+
options: [{
246+
callbacksLast: true
247+
}],
248+
parser: 'babel-eslint',
249+
ecmaFeatures: {
250+
classes: true,
251+
jsx: true
252+
}
253+
}, {
254+
code: [
255+
'class First extends React.Component {',
256+
' render() {',
257+
' return <div />;',
258+
' }',
259+
'}',
260+
'First.propTypes = {',
261+
' a: React.PropTypes.any,',
262+
' z: React.PropTypes.string,',
263+
' onBar: React.PropTypes.func,',
264+
' onFoo: React.PropTypes.func',
265+
'};'
266+
].join('\n'),
267+
options: [{
268+
callbacksLast: true
269+
}],
270+
ecmaFeatures: {
271+
classes: true,
272+
jsx: true
273+
}
211274
}],
212275

213276
invalid: [{
@@ -364,5 +427,112 @@ ruleTester.run('jsx-sort-prop-types', rule, {
364427
jsx: true
365428
},
366429
errors: 2
430+
}, {
431+
code: [
432+
'var First = React.createClass({',
433+
' propTypes: {',
434+
' a: React.PropTypes.any,',
435+
' z: React.PropTypes.string,',
436+
' onFoo: React.PropTypes.func,',
437+
' onBar: React.PropTypes.func',
438+
' },',
439+
' render: function() {',
440+
' return <div />;',
441+
' }',
442+
'});'
443+
].join('\n'),
444+
options: [{
445+
callbacksLast: true
446+
}],
447+
ecmaFeatures: {
448+
jsx: true
449+
},
450+
errors: [{
451+
message: ERROR_MESSAGE,
452+
line: 6,
453+
column: 5,
454+
type: 'Property'
455+
}]
456+
}, {
457+
code: [
458+
'class Component extends React.Component {',
459+
' static propTypes = {',
460+
' a: React.PropTypes.any,',
461+
' z: React.PropTypes.string,',
462+
' onFoo: React.PropTypes.func,',
463+
' onBar: React.PropTypes.func',
464+
' }',
465+
' render() {',
466+
' return <div />;',
467+
' }',
468+
'}'
469+
].join('\n'),
470+
options: [{
471+
callbacksLast: true
472+
}],
473+
parser: 'babel-eslint',
474+
ecmaFeatures: {
475+
classes: true,
476+
jsx: true
477+
},
478+
errors: [{
479+
message: ERROR_MESSAGE,
480+
line: 6,
481+
column: 5,
482+
type: 'Property'
483+
}]
484+
}, {
485+
code: [
486+
'class First extends React.Component {',
487+
' render() {',
488+
' return <div />;',
489+
' }',
490+
'}',
491+
'First.propTypes = {',
492+
' a: React.PropTypes.any,',
493+
' z: React.PropTypes.string,',
494+
' onFoo: React.PropTypes.func,',
495+
' onBar: React.PropTypes.func',
496+
'};'
497+
].join('\n'),
498+
options: [{
499+
callbacksLast: true
500+
}],
501+
ecmaFeatures: {
502+
classes: true,
503+
jsx: true
504+
},
505+
errors: [{
506+
message: ERROR_MESSAGE,
507+
line: 10,
508+
column: 5,
509+
type: 'Property'
510+
}]
511+
}, {
512+
code: [
513+
'var First = React.createClass({',
514+
' propTypes: {',
515+
' a: React.PropTypes.any,',
516+
' onBar: React.PropTypes.func,',
517+
' onFoo: React.PropTypes.func,',
518+
' z: React.PropTypes.string',
519+
' },',
520+
' render: function() {',
521+
' return <div />;',
522+
' }',
523+
'});'
524+
].join('\n'),
525+
options: [{
526+
callbacksLast: true
527+
}],
528+
ecmaFeatures: {
529+
jsx: true
530+
},
531+
errors: [{
532+
message: 'Callback prop types must be listed after all other prop types',
533+
line: 5,
534+
column: 5,
535+
type: 'Property'
536+
}]
367537
}]
368538
});

0 commit comments

Comments
 (0)