Skip to content

Commit 7bbbda7

Browse files
committed
Merge pull request #242 from Daniel15/jsx-callbacks-last
Update `jsx-sort-props` to allow sorting callbacks last
2 parents 677c70a + 73d125f commit 7bbbda7

File tree

6 files changed

+275
-12
lines changed

6 files changed

+275
-12
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: 12 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

@@ -38,6 +41,14 @@ The following patterns are considered okay and do not cause warnings:
3841
<Hello name="John" Number="2" />;
3942
```
4043

44+
### `callbacksLast`
45+
46+
When `true`, callbacks must be listed after all other props:
47+
48+
```js
49+
<Hello tel={5555555} onClick={this._handleClick} />
50+
```
51+
4152
## When not to use
4253

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

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: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
// Rule Definition
99
// ------------------------------------------------------------------------------
1010

11+
function isCallbackPropName(propName) {
12+
return /^on[A-Z]/.test(propName);
13+
}
14+
1115
module.exports = function(context) {
1216

1317
var configuration = context.options[0] || {};
1418
var ignoreCase = configuration.ignoreCase || false;
19+
var callbacksLast = configuration.callbacksLast || false;
1520

1621
return {
1722
JSXOpeningElement: function(node) {
@@ -20,15 +25,29 @@ module.exports = function(context) {
2025
return attrs[idx + 1];
2126
}
2227

23-
var lastPropName = memo.name.name;
24-
var currenPropName = decl.name.name;
28+
var previousPropName = memo.name.name;
29+
var currentPropName = decl.name.name;
30+
var previousIsCallback = isCallbackPropName(previousPropName);
31+
var currentIsCallback = isCallbackPropName(currentPropName);
2532

2633
if (ignoreCase) {
27-
lastPropName = lastPropName.toLowerCase();
28-
currenPropName = currenPropName.toLowerCase();
34+
previousPropName = previousPropName.toLowerCase();
35+
currentPropName = currentPropName.toLowerCase();
36+
}
37+
38+
if (callbacksLast) {
39+
if (!previousIsCallback && currentIsCallback) {
40+
// Entering the callback prop section
41+
return decl;
42+
}
43+
if (previousIsCallback && !currentIsCallback) {
44+
// Encountered a non-callback prop after a callback prop
45+
context.report(memo, 'Callbacks must be listed after all other props');
46+
return memo;
47+
}
2948
}
3049

31-
if (currenPropName < lastPropName) {
50+
if (currentPropName < previousPropName) {
3251
context.report(decl, 'Props should be sorted alphabetically');
3352
return memo;
3453
}
@@ -42,6 +61,11 @@ module.exports = function(context) {
4261
module.exports.schema = [{
4362
type: 'object',
4463
properties: {
64+
// Whether callbacks (prefixed with "on") should be listed at the very end,
65+
// after all other props.
66+
callbacksLast: {
67+
type: 'boolean'
68+
},
4569
ignoreCase: {
4670
type: 'boolean'
4771
}

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)