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