sort-comp.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /**
  2. * @fileoverview Enforce component methods order
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const entries = require('object.entries');
  8. const arrayIncludes = require('array-includes');
  9. const Components = require('../util/Components');
  10. const astUtil = require('../util/ast');
  11. const docsUrl = require('../util/docsUrl');
  12. const defaultConfig = {
  13. order: [
  14. 'static-methods',
  15. 'lifecycle',
  16. 'everything-else',
  17. 'render'
  18. ],
  19. groups: {
  20. lifecycle: [
  21. 'displayName',
  22. 'propTypes',
  23. 'contextTypes',
  24. 'childContextTypes',
  25. 'mixins',
  26. 'statics',
  27. 'defaultProps',
  28. 'constructor',
  29. 'getDefaultProps',
  30. 'state',
  31. 'getInitialState',
  32. 'getChildContext',
  33. 'getDerivedStateFromProps',
  34. 'componentWillMount',
  35. 'UNSAFE_componentWillMount',
  36. 'componentDidMount',
  37. 'componentWillReceiveProps',
  38. 'UNSAFE_componentWillReceiveProps',
  39. 'shouldComponentUpdate',
  40. 'componentWillUpdate',
  41. 'UNSAFE_componentWillUpdate',
  42. 'getSnapshotBeforeUpdate',
  43. 'componentDidUpdate',
  44. 'componentDidCatch',
  45. 'componentWillUnmount'
  46. ]
  47. }
  48. };
  49. /**
  50. * Get the methods order from the default config and the user config
  51. * @param {Object} userConfig The user configuration.
  52. * @returns {Array} Methods order
  53. */
  54. function getMethodsOrder(userConfig) {
  55. userConfig = userConfig || {};
  56. const groups = Object.assign({}, defaultConfig.groups, userConfig.groups);
  57. const order = userConfig.order || defaultConfig.order;
  58. let config = [];
  59. let entry;
  60. for (let i = 0, j = order.length; i < j; i++) {
  61. entry = order[i];
  62. if (has(groups, entry)) {
  63. config = config.concat(groups[entry]);
  64. } else {
  65. config.push(entry);
  66. }
  67. }
  68. return config;
  69. }
  70. // ------------------------------------------------------------------------------
  71. // Rule Definition
  72. // ------------------------------------------------------------------------------
  73. module.exports = {
  74. meta: {
  75. docs: {
  76. description: 'Enforce component methods order',
  77. category: 'Stylistic Issues',
  78. recommended: false,
  79. url: docsUrl('sort-comp')
  80. },
  81. messages: {
  82. unsortedProps: '{{propA}} should be placed {{position}} {{propB}}'
  83. },
  84. schema: [{
  85. type: 'object',
  86. properties: {
  87. order: {
  88. type: 'array',
  89. items: {
  90. type: 'string'
  91. }
  92. },
  93. groups: {
  94. type: 'object',
  95. patternProperties: {
  96. '^.*$': {
  97. type: 'array',
  98. items: {
  99. type: 'string'
  100. }
  101. }
  102. }
  103. }
  104. },
  105. additionalProperties: false
  106. }]
  107. },
  108. create: Components.detect((context, components) => {
  109. const errors = {};
  110. const methodsOrder = getMethodsOrder(context.options[0]);
  111. // --------------------------------------------------------------------------
  112. // Public
  113. // --------------------------------------------------------------------------
  114. const regExpRegExp = /\/(.*)\/([gimsuy]*)/;
  115. /**
  116. * Get indexes of the matching patterns in methods order configuration
  117. * @param {Object} method - Method metadata.
  118. * @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match.
  119. */
  120. function getRefPropIndexes(method) {
  121. const methodGroupIndexes = [];
  122. methodsOrder.forEach((currentGroup, groupIndex) => {
  123. if (currentGroup === 'getters') {
  124. if (method.getter) {
  125. methodGroupIndexes.push(groupIndex);
  126. }
  127. } else if (currentGroup === 'setters') {
  128. if (method.setter) {
  129. methodGroupIndexes.push(groupIndex);
  130. }
  131. } else if (currentGroup === 'type-annotations') {
  132. if (method.typeAnnotation) {
  133. methodGroupIndexes.push(groupIndex);
  134. }
  135. } else if (currentGroup === 'static-variables') {
  136. if (method.staticVariable) {
  137. methodGroupIndexes.push(groupIndex);
  138. }
  139. } else if (currentGroup === 'static-methods') {
  140. if (method.staticMethod) {
  141. methodGroupIndexes.push(groupIndex);
  142. }
  143. } else if (currentGroup === 'instance-variables') {
  144. if (method.instanceVariable) {
  145. methodGroupIndexes.push(groupIndex);
  146. }
  147. } else if (currentGroup === 'instance-methods') {
  148. if (method.instanceMethod) {
  149. methodGroupIndexes.push(groupIndex);
  150. }
  151. } else if (arrayIncludes([
  152. 'displayName',
  153. 'propTypes',
  154. 'contextTypes',
  155. 'childContextTypes',
  156. 'mixins',
  157. 'statics',
  158. 'defaultProps',
  159. 'constructor',
  160. 'getDefaultProps',
  161. 'state',
  162. 'getInitialState',
  163. 'getChildContext',
  164. 'getDerivedStateFromProps',
  165. 'componentWillMount',
  166. 'UNSAFE_componentWillMount',
  167. 'componentDidMount',
  168. 'componentWillReceiveProps',
  169. 'UNSAFE_componentWillReceiveProps',
  170. 'shouldComponentUpdate',
  171. 'componentWillUpdate',
  172. 'UNSAFE_componentWillUpdate',
  173. 'getSnapshotBeforeUpdate',
  174. 'componentDidUpdate',
  175. 'componentDidCatch',
  176. 'componentWillUnmount',
  177. 'render'
  178. ], currentGroup)) {
  179. if (currentGroup === method.name) {
  180. methodGroupIndexes.push(groupIndex);
  181. }
  182. } else {
  183. // Is the group a regex?
  184. const isRegExp = currentGroup.match(regExpRegExp);
  185. if (isRegExp) {
  186. const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
  187. if (isMatching) {
  188. methodGroupIndexes.push(groupIndex);
  189. }
  190. } else if (currentGroup === method.name) {
  191. methodGroupIndexes.push(groupIndex);
  192. }
  193. }
  194. });
  195. // No matching pattern, return 'everything-else' index
  196. if (methodGroupIndexes.length === 0) {
  197. const everythingElseIndex = methodsOrder.indexOf('everything-else');
  198. if (everythingElseIndex !== -1) {
  199. methodGroupIndexes.push(everythingElseIndex);
  200. } else {
  201. // No matching pattern and no 'everything-else' group
  202. methodGroupIndexes.push(Infinity);
  203. }
  204. }
  205. return methodGroupIndexes;
  206. }
  207. /**
  208. * Get properties name
  209. * @param {Object} node - Property.
  210. * @returns {String} Property name.
  211. */
  212. function getPropertyName(node) {
  213. if (node.kind === 'get') {
  214. return 'getter functions';
  215. }
  216. if (node.kind === 'set') {
  217. return 'setter functions';
  218. }
  219. return astUtil.getPropertyName(node);
  220. }
  221. /**
  222. * Store a new error in the error list
  223. * @param {Object} propA - Mispositioned property.
  224. * @param {Object} propB - Reference property.
  225. */
  226. function storeError(propA, propB) {
  227. // Initialize the error object if needed
  228. if (!errors[propA.index]) {
  229. errors[propA.index] = {
  230. node: propA.node,
  231. score: 0,
  232. closest: {
  233. distance: Infinity,
  234. ref: {
  235. node: null,
  236. index: 0
  237. }
  238. }
  239. };
  240. }
  241. // Increment the prop score
  242. errors[propA.index].score++;
  243. // Stop here if we already have pushed another node at this position
  244. if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) {
  245. return;
  246. }
  247. // Stop here if we already have a closer reference
  248. if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) {
  249. return;
  250. }
  251. // Update the closest reference
  252. errors[propA.index].closest.distance = Math.abs(propA.index - propB.index);
  253. errors[propA.index].closest.ref.node = propB.node;
  254. errors[propA.index].closest.ref.index = propB.index;
  255. }
  256. /**
  257. * Dedupe errors, only keep the ones with the highest score and delete the others
  258. */
  259. function dedupeErrors() {
  260. for (const i in errors) {
  261. if (has(errors, i)) {
  262. const index = errors[i].closest.ref.index;
  263. if (errors[index]) {
  264. if (errors[i].score > errors[index].score) {
  265. delete errors[index];
  266. } else {
  267. delete errors[i];
  268. }
  269. }
  270. }
  271. }
  272. }
  273. /**
  274. * Report errors
  275. */
  276. function reportErrors() {
  277. dedupeErrors();
  278. entries(errors).forEach((entry) => {
  279. const nodeA = entry[1].node;
  280. const nodeB = entry[1].closest.ref.node;
  281. const indexA = entry[0];
  282. const indexB = entry[1].closest.ref.index;
  283. context.report({
  284. node: nodeA,
  285. messageId: 'unsortedProps',
  286. data: {
  287. propA: getPropertyName(nodeA),
  288. propB: getPropertyName(nodeB),
  289. position: indexA < indexB ? 'before' : 'after'
  290. }
  291. });
  292. });
  293. }
  294. /**
  295. * Compare two properties and find out if they are in the right order
  296. * @param {Array} propertiesInfos Array containing all the properties metadata.
  297. * @param {Object} propA First property name and metadata
  298. * @param {Object} propB Second property name.
  299. * @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties.
  300. */
  301. function comparePropsOrder(propertiesInfos, propA, propB) {
  302. let i;
  303. let j;
  304. let k;
  305. let l;
  306. let refIndexA;
  307. let refIndexB;
  308. // Get references indexes (the correct position) for given properties
  309. const refIndexesA = getRefPropIndexes(propA);
  310. const refIndexesB = getRefPropIndexes(propB);
  311. // Get current indexes for given properties
  312. const classIndexA = propertiesInfos.indexOf(propA);
  313. const classIndexB = propertiesInfos.indexOf(propB);
  314. // Loop around the references indexes for the 1st property
  315. for (i = 0, j = refIndexesA.length; i < j; i++) {
  316. refIndexA = refIndexesA[i];
  317. // Loop around the properties for the 2nd property (for comparison)
  318. for (k = 0, l = refIndexesB.length; k < l; k++) {
  319. refIndexB = refIndexesB[k];
  320. if (
  321. // Comparing the same properties
  322. refIndexA === refIndexB
  323. // 1st property is placed before the 2nd one in reference and in current component
  324. || ((refIndexA < refIndexB) && (classIndexA < classIndexB))
  325. // 1st property is placed after the 2nd one in reference and in current component
  326. || ((refIndexA > refIndexB) && (classIndexA > classIndexB))
  327. ) {
  328. return {
  329. correct: true,
  330. indexA: classIndexA,
  331. indexB: classIndexB
  332. };
  333. }
  334. }
  335. }
  336. // We did not find any correct match between reference and current component
  337. return {
  338. correct: false,
  339. indexA: refIndexA,
  340. indexB: refIndexB
  341. };
  342. }
  343. /**
  344. * Check properties order from a properties list and store the eventual errors
  345. * @param {Array} properties Array containing all the properties.
  346. */
  347. function checkPropsOrder(properties) {
  348. const propertiesInfos = properties.map((node) => ({
  349. name: getPropertyName(node),
  350. getter: node.kind === 'get',
  351. setter: node.kind === 'set',
  352. staticVariable: node.static
  353. && node.type === 'ClassProperty'
  354. && (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
  355. staticMethod: node.static
  356. && (node.type === 'ClassProperty' || node.type === 'MethodDefinition')
  357. && node.value
  358. && (astUtil.isFunctionLikeExpression(node.value)),
  359. instanceVariable: !node.static
  360. && node.type === 'ClassProperty'
  361. && (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
  362. instanceMethod: !node.static
  363. && node.type === 'ClassProperty'
  364. && node.value
  365. && (astUtil.isFunctionLikeExpression(node.value)),
  366. typeAnnotation: !!node.typeAnnotation && node.value === null
  367. }));
  368. // Loop around the properties
  369. propertiesInfos.forEach((propA, i) => {
  370. // Loop around the properties a second time (for comparison)
  371. propertiesInfos.forEach((propB, k) => {
  372. if (i === k) {
  373. return;
  374. }
  375. // Compare the properties order
  376. const order = comparePropsOrder(propertiesInfos, propA, propB);
  377. if (!order.correct) {
  378. // Store an error if the order is incorrect
  379. storeError({
  380. node: properties[i],
  381. index: order.indexA
  382. }, {
  383. node: properties[k],
  384. index: order.indexB
  385. });
  386. }
  387. });
  388. });
  389. }
  390. return {
  391. 'Program:exit'() {
  392. const list = components.list();
  393. Object.keys(list).forEach((component) => {
  394. const properties = astUtil.getComponentProperties(list[component].node);
  395. checkPropsOrder(properties);
  396. });
  397. reportErrors();
  398. }
  399. };
  400. }),
  401. defaultConfig
  402. };