Skip to content

Commit 36beb6d

Browse files
authored
Merge pull request #1483 from b0gok/jsx-sort-default-props
Add jsx-sort-default-props rule
2 parents c2c7a2a + e24b53f commit 36beb6d

File tree

5 files changed

+966
-0
lines changed

5 files changed

+966
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Enable the rules that you would like to use.
156156
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
157157
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
158158
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
159+
* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting
159160
* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
160161
* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)
161162
* [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable)

docs/rules/jsx-sort-default-props.md

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Enforce defaultProps declarations alphabetical sorting (react/jsx-sort-default-props)
2+
3+
Some developers prefer to sort `defaultProps` declarations alphabetically to be able to find necessary declarations easier at a later time. Others feel that it adds complexity and becomes a burden to maintain.
4+
5+
## Rule Details
6+
7+
This rule checks all components and verifies that all `defaultProps` declarations are sorted alphabetically. A spread attribute resets the verification. The default configuration of the rule is case-sensitive.
8+
9+
The following patterns are considered warnings:
10+
11+
```jsx
12+
var Component = createReactClass({
13+
...
14+
getDefaultProps: function() {
15+
return {
16+
z: "z",
17+
a: "a",
18+
b: "b"
19+
};
20+
},
21+
...
22+
});
23+
24+
class Component extends React.Component {
25+
...
26+
}
27+
Component.defaultProps = {
28+
z: "z",
29+
a: "a",
30+
b: "b"
31+
};
32+
33+
class Component extends React.Component {
34+
static defaultProps = {
35+
z: "z",
36+
y: "y",
37+
a: "a"
38+
}
39+
render() {
40+
return <div />;
41+
}
42+
}
43+
44+
const Component = (props) => (...);
45+
Component.defaultProps = {
46+
z: "z",
47+
y: "y",
48+
a: "a"
49+
};
50+
51+
const defaults = {
52+
b: "b"
53+
};
54+
const types = {
55+
a: PropTypes.string,
56+
b: PropTypes.string,
57+
c: PropTypes.string'
58+
};
59+
function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
60+
return <div>{a}{b}{c}</div>;
61+
}
62+
StatelessComponentWithSpreadInPropTypes.propTypes = types;
63+
StatelessComponentWithSpreadInPropTypes.defaultProps = {
64+
c: "c",
65+
a: "a",
66+
...defaults,
67+
};
68+
69+
export default class ClassWithSpreadInPropTypes extends BaseClass {
70+
static propTypes = {
71+
a: PropTypes.string,
72+
b: PropTypes.string,
73+
c: PropTypes.string,
74+
d: PropTypes.string,
75+
e: PropTypes.string,
76+
f: PropTypes.string
77+
}
78+
static defaultProps = {
79+
b: "b",
80+
a: "a",
81+
...c.defaultProps,
82+
f: "f",
83+
e: "e",
84+
...d.defaultProps
85+
}
86+
}
87+
```
88+
89+
The following patterns are considered okay and do **not** cause warnings:
90+
91+
```jsx
92+
var Component = createReactClass({
93+
...
94+
getDefaultProps: function() {
95+
return {
96+
a: "a",
97+
b: "b",
98+
c: "c"
99+
};
100+
},
101+
...
102+
});
103+
104+
class Component extends React.Component {
105+
...
106+
}
107+
Component.defaultProps = {
108+
a: "a",
109+
b: "b",
110+
c: "c"
111+
};
112+
113+
class Component extends React.Component {
114+
static defaultProps = {
115+
a: PropTypes.any,
116+
b: PropTypes.any,
117+
c: PropTypes.any
118+
}
119+
render() {
120+
return <div />;
121+
}
122+
}
123+
124+
const Component = (props) => (...);
125+
Component.defaultProps = {
126+
a: "a",
127+
y: "y",
128+
z: "z"
129+
};
130+
131+
const defaults = {
132+
b: "b"
133+
};
134+
const types = {
135+
a: PropTypes.string,
136+
b: PropTypes.string,
137+
c: PropTypes.string'
138+
};
139+
function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
140+
return <div>{a}{b}{c}</div>;
141+
}
142+
StatelessComponentWithSpreadInPropTypes.propTypes = types;
143+
StatelessComponentWithSpreadInPropTypes.defaultProps = {
144+
a: "a",
145+
c: "c",
146+
...defaults,
147+
};
148+
149+
export default class ClassWithSpreadInPropTypes extends BaseClass {
150+
static propTypes = {
151+
a: PropTypes.string,
152+
b: PropTypes.string,
153+
c: PropTypes.string,
154+
d: PropTypes.string,
155+
e: PropTypes.string,
156+
f: PropTypes.string
157+
}
158+
static defaultProps = {
159+
a: "a",
160+
b: "b",
161+
...c.defaultProps,
162+
e: "e",
163+
f: "f",
164+
...d.defaultProps
165+
}
166+
}
167+
```
168+
169+
## Rule Options
170+
171+
```js
172+
...
173+
"react/jsx-sort-default-props": [<enabled>, {
174+
"ignoreCase": <boolean>,
175+
}]
176+
...
177+
```
178+
179+
### `ignoreCase`
180+
181+
When `true` the rule ignores the case-sensitivity of the declarations order.
182+
183+
## When not to use
184+
185+
This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing `defaultProps` declarations isn't a part of your coding standards, then you can leave this rule off.

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const allRules = {
3535
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
3636
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
3737
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
38+
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
3839
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
3940
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
4041
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),

lib/rules/jsx-sort-default-props.js

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* @fileoverview Enforce default props alphabetical sorting
3+
* @author Vladimir Kattsov
4+
*/
5+
'use strict';
6+
7+
const variableUtil = require('../util/variable');
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
module.exports = {
14+
meta: {
15+
docs: {
16+
description: 'Enforce default props alphabetical sorting',
17+
category: 'Stylistic Issues',
18+
recommended: false
19+
},
20+
21+
schema: [{
22+
type: 'object',
23+
properties: {
24+
ignoreCase: {
25+
type: 'boolean'
26+
}
27+
},
28+
additionalProperties: false
29+
}]
30+
},
31+
32+
create: function(context) {
33+
const sourceCode = context.getSourceCode();
34+
const configuration = context.options[0] || {};
35+
const ignoreCase = configuration.ignoreCase || false;
36+
const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
37+
38+
/**
39+
* Get properties name
40+
* @param {Object} node - Property.
41+
* @returns {String} Property name.
42+
*/
43+
function getPropertyName(node) {
44+
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
45+
return node.key.name;
46+
} else if (node.type === 'MemberExpression') {
47+
return node.property.name;
48+
// Special case for class properties
49+
// (babel-eslint@5 does not expose property name so we have to rely on tokens)
50+
} else if (node.type === 'ClassProperty') {
51+
const tokens = context.getFirstTokens(node, 2);
52+
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
53+
}
54+
return '';
55+
}
56+
57+
/**
58+
* Checks if the Identifier node passed in looks like a defaultProps declaration.
59+
* @param {ASTNode} node The node to check. Must be an Identifier node.
60+
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
61+
*/
62+
function isDefaultPropsDeclaration(node) {
63+
const propName = getPropertyName(node);
64+
return (propName === 'defaultProps' || propName === 'getDefaultProps');
65+
}
66+
67+
function getKey(node) {
68+
return sourceCode.getText(node.key || node.argument);
69+
}
70+
71+
/**
72+
* Find a variable by name in the current scope.
73+
* @param {string} name Name of the variable to look for.
74+
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
75+
*/
76+
function findVariableByName(name) {
77+
const variable = variableUtil.variablesInScope(context).find(item => item.name === name);
78+
79+
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
80+
return null;
81+
}
82+
83+
if (variable.defs[0].node.type === 'TypeAlias') {
84+
return variable.defs[0].node.right;
85+
}
86+
87+
return variable.defs[0].node.init;
88+
}
89+
90+
/**
91+
* Checks if defaultProps declarations are sorted
92+
* @param {Array} declarations The array of AST nodes being checked.
93+
* @returns {void}
94+
*/
95+
function checkSorted(declarations) {
96+
declarations.reduce((prev, curr, idx, decls) => {
97+
if (/SpreadProperty$/.test(curr.type)) {
98+
return decls[idx + 1];
99+
}
100+
101+
let prevPropName = getKey(prev);
102+
let currentPropName = getKey(curr);
103+
104+
if (ignoreCase) {
105+
prevPropName = prevPropName.toLowerCase();
106+
currentPropName = currentPropName.toLowerCase();
107+
}
108+
109+
if (currentPropName < prevPropName) {
110+
context.report({
111+
node: curr,
112+
message: 'Default prop types declarations should be sorted alphabetically'
113+
});
114+
115+
return prev;
116+
}
117+
118+
return curr;
119+
}, declarations[0]);
120+
}
121+
122+
function checkNode(node) {
123+
switch (node && node.type) {
124+
case 'ObjectExpression':
125+
checkSorted(node.properties);
126+
break;
127+
case 'Identifier':
128+
const propTypesObject = findVariableByName(node.name);
129+
if (propTypesObject && propTypesObject.properties) {
130+
checkSorted(propTypesObject.properties);
131+
}
132+
break;
133+
case 'CallExpression':
134+
const innerNode = node.arguments && node.arguments[0];
135+
if (propWrapperFunctions.has(node.callee.name) && innerNode) {
136+
checkNode(innerNode);
137+
}
138+
break;
139+
default:
140+
break;
141+
}
142+
}
143+
144+
// --------------------------------------------------------------------------
145+
// Public API
146+
// --------------------------------------------------------------------------
147+
148+
return {
149+
ClassProperty: function(node) {
150+
if (!isDefaultPropsDeclaration(node)) {
151+
return;
152+
}
153+
154+
checkNode(node.value);
155+
},
156+
157+
MemberExpression: function(node) {
158+
if (!isDefaultPropsDeclaration(node)) {
159+
return;
160+
}
161+
162+
checkNode(node.parent.right);
163+
}
164+
};
165+
}
166+
};

0 commit comments

Comments
 (0)