sort-comp.js 13 KB

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