diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-default-props.input.js b/transforms/__testfixtures__/sort-comp/sort-comp-default-props.input.js
new file mode 100644
index 0000000..6f33cf5
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-default-props.input.js
@@ -0,0 +1,37 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.Component {
+ componentDidMount() {
+
+ }
+
+ static defaultProps = {
+ foo: 42,
+ };
+
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return
;
+ }
+
+ // comment on constructor
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ // should be before defaultProps
+ static propTypes = {
+
+ };
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+}
+
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-default-props.output.js b/transforms/__testfixtures__/sort-comp/sort-comp-default-props.output.js
new file mode 100644
index 0000000..f2958d8
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-default-props.output.js
@@ -0,0 +1,37 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.Component {
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ // should be before defaultProps
+ static propTypes = {
+
+ };
+
+ static defaultProps = {
+ foo: 42,
+ };
+
+ // comment on constructor
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ componentDidMount() {
+
+ }
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return ;
+ }
+}
+
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-full.input.js b/transforms/__testfixtures__/sort-comp/sort-comp-full.input.js
new file mode 100644
index 0000000..9705601
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-full.input.js
@@ -0,0 +1,76 @@
+import React from 'react';
+
+export class SortCompExample extends React.Component {
+ model = new Date();
+
+ static y = 8;
+
+ static x = 42;
+
+ static propTypes = {};
+
+ componentDidMount() {
+ console.log('componentDidMount');
+ }
+
+ constructor(props, context) {
+ super(props, context);
+
+ console.log(this);
+ }
+
+ static defaultProps = {
+ disabled: false,
+ };
+
+ someMethod2 = () => {};
+
+ state = {
+ isDataReady: false,
+ };
+
+ onBar() {}
+
+ handleBar = () => {
+ };
+
+ set foo(value) {
+ this.xx = value;
+ }
+
+ handleBar2() {}
+
+ handleFoo() {}
+
+ handleFoo1 = () => {};
+
+ onBar1 = () => {};
+
+ renderFoo() {}
+
+ onFoo() {}
+
+ onFoo1 = () => {};
+
+ get foo() {
+ return 42;
+ }
+
+ static track() {}
+
+ setRef = (comp) => {
+ this.panelRef = comp;
+ };
+
+ methodFoo = () => {};
+
+ someMethod1() {
+ return 42;
+ }
+
+ renderBar() {}
+
+ render() {
+ return (X
);
+ }
+}
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-full.output.js b/transforms/__testfixtures__/sort-comp/sort-comp-full.output.js
new file mode 100644
index 0000000..6cc3ca5
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-full.output.js
@@ -0,0 +1,76 @@
+import React from 'react';
+
+export class SortCompExample extends React.Component {
+ static x = 42;
+
+ static y = 8;
+
+ static track() {}
+
+ model = new Date();
+
+ static propTypes = {};
+
+ static defaultProps = {
+ disabled: false,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ console.log(this);
+ }
+
+ state = {
+ isDataReady: false,
+ };
+
+ componentDidMount() {
+ console.log('componentDidMount');
+ }
+
+ handleBar = () => {
+ };
+
+ handleBar2() {}
+
+ handleFoo() {}
+
+ handleFoo1 = () => {};
+
+ onBar() {}
+
+ onBar1 = () => {};
+
+ onFoo() {}
+
+ onFoo1 = () => {};
+
+ get foo() {
+ return 42;
+ }
+
+ set foo(value) {
+ this.xx = value;
+ }
+
+ setRef = (comp) => {
+ this.panelRef = comp;
+ };
+
+ methodFoo = () => {};
+
+ someMethod2 = () => {};
+
+ someMethod1() {
+ return 42;
+ }
+
+ renderBar() {}
+
+ renderFoo() {}
+
+ render() {
+ return (X
);
+ }
+}
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.input.js b/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.input.js
new file mode 100644
index 0000000..31c5ea0
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.input.js
@@ -0,0 +1,54 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.PureComponent {
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return ;
+ }
+
+ // comment on componentDidMount
+ componentDidMount() {
+ }
+
+ // foo getter comment
+ get foo() {
+ return 42;
+ }
+
+ set foo(value) {
+ this.instanceVariable = value;
+ }
+
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ get bar() {
+
+ }
+
+ set bar(value) {
+
+ }
+
+ renderFoo() {
+ // other render* function
+ }
+
+ instanceVariable;
+
+ renderBar() {
+ // should come before renderFoo
+ }
+
+ static aStaticThing() {
+ // should come first
+ }
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+}
+
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.output.js b/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.output.js
new file mode 100644
index 0000000..2864745
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-getters-setters.output.js
@@ -0,0 +1,54 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.PureComponent {
+ static aStaticThing() {
+ // should come first
+ }
+
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ instanceVariable;
+
+ // comment on componentDidMount
+ componentDidMount() {
+ }
+
+ get bar() {
+
+ }
+
+ // foo getter comment
+ get foo() {
+ return 42;
+ }
+
+ set bar(value) {
+
+ }
+
+ set foo(value) {
+ this.instanceVariable = value;
+ }
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+
+ renderBar() {
+ // should come before renderFoo
+ }
+
+ renderFoo() {
+ // other render* function
+ }
+
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return ;
+ }
+}
+
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.input.js b/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.input.js
new file mode 100644
index 0000000..20428d2
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.input.js
@@ -0,0 +1,43 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.Component {
+
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return ;
+ }
+
+ // comment on componentDidMount
+ componentDidMount() {
+ }
+
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ renderFoo() {
+ // other render* function
+ }
+
+ foo;
+
+ bar = null;
+
+ renderBar() {
+ // should come before renderFoo
+ }
+
+ model = new Date();
+
+ static aStaticThing() {
+ // should come first
+ }
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+
+}
+
diff --git a/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.output.js b/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.output.js
new file mode 100644
index 0000000..86d7746
--- /dev/null
+++ b/transforms/__testfixtures__/sort-comp/sort-comp-instance-vars.output.js
@@ -0,0 +1,41 @@
+import React from 'react/addons';
+
+// comment above class
+export class MyComponent extends React.Component {
+ static aStaticThing() {
+ // should come first
+ }
+
+ static someStaticThing() {
+ // should bundle with other statics
+ }
+
+ bar = null;
+
+ foo;
+
+ model = new Date();
+
+ // comment on componentDidMount
+ componentDidMount() {
+ }
+
+ myOwnMethod(foo) {
+ // comment within method
+ }
+
+ renderBar() {
+ // should come before renderFoo
+ }
+
+ renderFoo() {
+ // other render* function
+ }
+
+ // comment related to render method
+ // this will be attached to render method
+ render() {
+ return ;
+ }
+}
+
diff --git a/transforms/__testfixtures__/sort-comp-pure.input.js b/transforms/__testfixtures__/sort-comp/sort-comp-pure.input.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp-pure.input.js
rename to transforms/__testfixtures__/sort-comp/sort-comp-pure.input.js
diff --git a/transforms/__testfixtures__/sort-comp-pure.output.js b/transforms/__testfixtures__/sort-comp/sort-comp-pure.output.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp-pure.output.js
rename to transforms/__testfixtures__/sort-comp/sort-comp-pure.output.js
diff --git a/transforms/__testfixtures__/sort-comp.input.js b/transforms/__testfixtures__/sort-comp/sort-comp.input.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp.input.js
rename to transforms/__testfixtures__/sort-comp/sort-comp.input.js
diff --git a/transforms/__testfixtures__/sort-comp.output.js b/transforms/__testfixtures__/sort-comp/sort-comp.output.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp.output.js
rename to transforms/__testfixtures__/sort-comp/sort-comp.output.js
diff --git a/transforms/__testfixtures__/sort-comp2.input.js b/transforms/__testfixtures__/sort-comp/sort-comp2.input.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp2.input.js
rename to transforms/__testfixtures__/sort-comp/sort-comp2.input.js
diff --git a/transforms/__testfixtures__/sort-comp2.output.js b/transforms/__testfixtures__/sort-comp/sort-comp2.output.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp2.output.js
rename to transforms/__testfixtures__/sort-comp/sort-comp2.output.js
diff --git a/transforms/__testfixtures__/sort-comp3.input.js b/transforms/__testfixtures__/sort-comp/sort-comp3.input.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp3.input.js
rename to transforms/__testfixtures__/sort-comp/sort-comp3.input.js
diff --git a/transforms/__testfixtures__/sort-comp3.output.js b/transforms/__testfixtures__/sort-comp/sort-comp3.output.js
similarity index 100%
rename from transforms/__testfixtures__/sort-comp3.output.js
rename to transforms/__testfixtures__/sort-comp/sort-comp3.output.js
diff --git a/transforms/__tests__/sort-comp-test.js b/transforms/__tests__/sort-comp-test.js
index bdeb344..1d6f0b5 100644
--- a/transforms/__tests__/sort-comp-test.js
+++ b/transforms/__tests__/sort-comp-test.js
@@ -2,14 +2,18 @@
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
+ * LICENSE file in the root directory of this source tree.
*
*/
'use strict';
const defineTest = require('jscodeshift/dist/testUtils').defineTest;
-defineTest(__dirname, 'sort-comp');
-defineTest(__dirname, 'sort-comp', null, 'sort-comp2');
-defineTest(__dirname, 'sort-comp', null, 'sort-comp3');
-defineTest(__dirname, 'sort-comp', null, 'sort-comp-pure');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp2');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp3');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp-pure');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp-instance-vars');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp-getters-setters');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp-default-props');
+defineTest(__dirname, 'sort-comp', null, 'sort-comp/sort-comp-full');
diff --git a/transforms/sort-comp.js b/transforms/sort-comp.js
index 6e8adf6..e94d9ab 100644
--- a/transforms/sort-comp.js
+++ b/transforms/sort-comp.js
@@ -12,15 +12,21 @@
* Reorders React component methods to match the [ESLint](http://eslint.org/)
* [react/sort-comp rule](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md),
* specifically with the [tighter constraints of the Airbnb style guide]
- * (https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e9b2748b9e/\
- * packages/eslint-config-airbnb/rules/react.js#L122-L134),
+ * (https://github.com/airbnb/javascript/blob/eslint-config-airbnb-v19.0.4/\
+ * packages/eslint-config-airbnb/rules/react.js#L243-L256),
*
* 'react/sort-comp': [2, {
* 'order': [
+ * 'static-variables',
* 'static-methods',
+ * 'instance-variables',
* 'lifecycle',
+ * '/^handle.+$/',
* '/^on.+$/',
+ * 'getters',
+ * 'setters',
* '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/',
+ * 'instance-methods',
* 'everything-else',
* '/^render.+$/',
* 'render'
@@ -52,8 +58,8 @@ module.exports = function(fileInfo, api, options) {
const sameLocation = indexA === indexB;
if (sameLocation) {
- // compare lexically
- return +(nameA > nameB) || +(nameA === nameB) - 1;
+ // compare lexically.
+ return nameA.localeCompare(nameB);
} else {
// compare by index
return indexA - indexB;
@@ -101,9 +107,9 @@ module.exports = function(fileInfo, api, options) {
return null;
};
-// Hard-coded for Airbnb style
-const defaultMethodsOrder = [
- 'static-methods',
+const REACT_LIFECYCLE_PLACEHOLDER = 'lifecycle';
+
+const REACT_LIFECYCLE_METHODS = [
'displayName',
'propTypes',
'contextTypes',
@@ -129,8 +135,20 @@ const defaultMethodsOrder = [
'componentDidUpdate',
'componentDidCatch',
'componentWillUnmount',
+];
+
+// Hard-coded for Airbnb style
+const defaultMethodsOrder = [
+ 'static-variables',
+ 'static-methods',
+ 'instance-variables',
+ REACT_LIFECYCLE_PLACEHOLDER,
+ '/^handle.+$/',
'/^on.+$/',
+ 'getters',
+ 'setters',
'/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/',
+ 'instance-methods',
'everything-else',
'/^render.+$/',
'render'
@@ -139,33 +157,7 @@ const defaultMethodsOrder = [
// FROM https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/sort-comp.js
const regExpRegExp = /\/(.*)\/([g|y|i|m]*)/;
-function selectorMatches(selector, method) {
- const methodName = method.key.name;
-
- if (
- method.static &&
- selector === 'static-methods' &&
- defaultMethodsOrder.indexOf(methodName) === -1
- ) {
- return true;
- }
-
- if (
- !method.value &&
- method.typeAnnotation &&
- selector === 'type-annotations'
- ) {
- return true;
- }
-
- if (method.static && selector === 'static-methods') {
- return true;
- }
-
- if (selector === methodName) {
- return true;
- }
-
+function isSelectorMatchesRegexp(selector, methodName) {
const selectorIsRe = regExpRegExp.test(selector);
if (selectorIsRe) {
@@ -177,16 +169,94 @@ function selectorMatches(selector, method) {
return false;
}
+function isSelectorMatchesClassDeclarations(selector, method) {
+ switch (selector) {
+ case 'static-variables': {
+ return (
+ method.static &&
+ method.type === 'ClassProperty' &&
+ method.value?.type !== 'ArrowFunctionExpression' &&
+ method.value?.type !== 'FunctionExpression'
+ );
+ }
+ case 'static-methods': {
+ return (
+ method.static &&
+ (
+ (method.type === 'ClassMethod' && !method.value) ||
+ (method.type === 'ClassProperty' &&
+ method.value?.type === 'ArrowFunctionExpression' ||
+ method.value?.type === 'FunctionExpression'
+ )
+ )
+ );
+ }
+ case 'type-annotations': {
+ return !method.value && method.typeAnnotation;
+ }
+ case 'instance-variables': {
+ return (
+ !method.static &&
+ method.type === 'ClassProperty' &&
+ method.value?.type !== 'ArrowFunctionExpression'
+ );
+ }
+ case 'instance-methods': {
+ return (
+ !method.static &&
+ method.type === 'ClassProperty' &&
+ method.value?.type === 'ArrowFunctionExpression'
+ );
+ }
+ case 'getters': {
+ return method.kind === 'get';
+ }
+ case 'setters': {
+ return method.kind === 'set';
+ }
+ default: {
+ return false;
+ }
+ }
+}
+
/**
* Get index of the matching patterns in methods order configuration
+ * @param {String[]} methodsOrder
* @param {Object} method
* @returns {Number} Index of the method in the method ordering. Return [Infinity] if there is no match.
*/
function getCorrectIndex(methodsOrder, method) {
+ const methodName = method.key.name;
+
+ /*
+ The same method could be matched by several criteria, say `static defaultProps = {}` can be matched
+ by static-variables and exact name criteria.
+ Here we introduce a priority to keep them organized.
+
+ priority:
+ 1 - match by exact name, say render, constructor, defaultProps, etc.
+ 2 - match by regexp
+ 3 - match by class declarations like static-variables, instance-methods etc.
+ 4 - match by everything-else
+ 5 - Infinity
+ */
+
+ const methodIndex = methodsOrder.indexOf(methodName);
+ if (methodIndex >= 0) {
+ return methodIndex;
+ }
+
+ const matchedRegexpIndex = methodsOrder.findIndex(selector => isSelectorMatchesRegexp(selector, methodName));
+
+ if (matchedRegexpIndex >= 0) {
+ return matchedRegexpIndex;
+ }
+
const everythingElseIndex = methodsOrder.indexOf('everything-else');
for (let i = 0; i < methodsOrder.length; i++) {
- if (i != everythingElseIndex && selectorMatches(methodsOrder[i], method)) {
+ if (i !== everythingElseIndex && isSelectorMatchesClassDeclarations(methodsOrder[i], method)) {
return i;
}
}
@@ -230,10 +300,23 @@ function getMethodsOrderFromEslint(filePath) {
return null;
}
+function expandArray(arr, token, variants) {
+ return arr.reduce((acc, item) => {
+ if (item === token) {
+ return [...acc, ...variants];
+ }
+
+ acc.push(item);
+ return acc;
+ }, []);
+}
+
function getMethodsOrder(fileInfo, options) {
- return (
+ const methodsOrders = (
options.methodsOrder ||
getMethodsOrderFromEslint(fileInfo.path) ||
defaultMethodsOrder
);
+
+ return expandArray(methodsOrders, REACT_LIFECYCLE_PLACEHOLDER, REACT_LIFECYCLE_METHODS);
}