sort-comp.js 13 KB

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