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