eslint-plugin-perfectionist icon indicating copy to clipboard operation
eslint-plugin-perfectionist copied to clipboard

Feature: trying to enforce a specific order within custom groups leads to complicated config

Open stormwarning opened this issue 11 months ago • 5 comments
trafficstars

What rule do you want to change?

sort-classes

Describe the problem

I'm using perfectionist/sort-classes as a replacement for react/sort-comp which is autofixable, but I'm also trying to follow Airbnb's class order, which leads to a very verbose config for the rule...

Code example

{
	type: 'natural',
	ignoreCase: false,
	groups: [
		'static-property',
		'static-method',
		'property',

		// React properties
		'displayName',
		'propTypes',
		'contextTypes',
		'childContextTypes',
		'mixins',
		'statics',
		'defaultProps',

		'constructor',

		// React lifecycle methods
		'getDefaultProps',
		'getInitialState',
		'state',
		'getChildContext',
		'getDerivedStateFromProps',
		'componentWillMount',
		'UNSAFE_componentWillMount',
		'componentDidMount',
		'componentWillReceiveProps',
		'UNSAFE_componentWillReceiveProps',
		'shouldComponentUpdate',
		'componentWillUpdate',
		'UNSAFE_componentWillUpdate',
		'getSnapshotBeforeUpdate',
		'componentDidUpdate',
		'componentDidCatch',
		'componentWillUnmount',

		'handlers',
		'callbacks',
		['get-method', 'set-method'],
		'get-set-methods',
		'method',
		'unknown',
		'render-methods',
		'main-render',
	],
	customGroups: [
		// React properties.
		{
			groupName: 'displayName',
			elementNamePattern: 'displayName',
		},
		{
			groupName: 'propTypes',
			elementNamePattern: 'propTypes',
		},
		{
			groupName: 'contextTypes',
			elementNamePattern: 'contextTypes',
		},
		{
			groupName: 'childContextTypes',
			elementNamePattern: 'childContextTypes',
		},
		{
			groupName: 'mixins',
			elementNamePattern: 'mixins',
		},
		{
			groupName: 'statics',
			elementNamePattern: 'statics',
		},
		{
			groupName: 'defaultProps',
			elementNamePattern: 'defaultProps',
		},

		// React lifecycle methods.
		{
			groupName: 'getDefaultProps',
			elementNamePattern: 'getDefaultProps',
		},
		{
			groupName: 'getInitialState',
			elementNamePattern: 'getInitialState',
		},
		{
			groupName: 'state',
			elementNamePattern: 'state',
		},
		{
			groupName: 'getChildContext',
			elementNamePattern: 'getChildContext',
		},
		{
			groupName: 'getDerivedStateFromProps',
			elementNamePattern: 'getDerivedStateFromProps',
		},
		{
			groupName: 'componentWillMount',
			elementNamePattern: 'componentWillMount',
		},
		{
			groupName: 'UNSAFE_componentWillMount',
			elementNamePattern: 'UNSAFE_componentWillMount',
		},
		{
			groupName: 'componentDidMount',
			elementNamePattern: 'componentDidMount',
		},
		{
			groupName: 'componentWillReceiveProps',
			elementNamePattern: 'componentWillReceiveProps',
		},
		{
			groupName: 'UNSAFE_componentWillReceiveProps',
			elementNamePattern: 'UNSAFE_componentWillReceiveProps',
		},
		{
			groupName: 'shouldComponentUpdate',
			elementNamePattern: 'shouldComponentUpdate',
		},
		{
			groupName: 'componentWillUpdate',
			elementNamePattern: 'componentWillUpdate',
		},
		{
			groupName: 'UNSAFE_componentWillUpdate',
			elementNamePattern: 'UNSAFE_componentWillUpdate',
		},
		{
			groupName: 'getSnapshotBeforeUpdate',
			elementNamePattern: 'getSnapshotBeforeUpdate',
		},
		{
			groupName: 'componentDidUpdate',
			elementNamePattern: 'componentDidUpdate',
		},
		{
			groupName: 'componentDidCatch',
			elementNamePattern: 'componentDidCatch',
		},
		{
			groupName: 'componentWillUnmount',
			elementNamePattern: 'componentWillUnmount',
		},

		{
			groupName: 'handlers',
			elementNamePattern: /^handle.+$/.source,
		},
		{
			groupName: 'callbacks',
			elementNamePattern: /^on.+$/.source,
		},
		{
			groupName: 'get-set-methods',
			elementNamePattern:
				/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/
					.source,
		},

		// Separate groups so that the main `render` comes last.
		{
			groupName: 'render-methods',
			elementNamePattern: /^render.+$/.source,
		},
		{
			groupName: 'main-render',
			elementNamePattern: 'render',
		},
	],
},

Additional comments

Is there any way to configure a specific order within custom groups that I'm missing?

Validations

  • [X] Read the docs.
  • [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.

stormwarning avatar Dec 13 '24 18:12 stormwarning

Hi @stormwarning

Could you please give a code example including:

  • The initial state or currently sorted state.
  • What you expect to happen instead.

This will help us better understand what feature you are looking for.

hugop95 avatar Dec 13 '24 18:12 hugop95

Oh, the config I'm using works, it's just not ideal for this case.

Ideally, I could define a single react-lifecycle custom group and define the exact order within it; something like

{
  groupName: 'react-lifecycle',
  elements: [
    'componentWillMount',
    'componentDidMount',
    // etc...
  ],
}

Where elements (or what have you) is a list of patterns. Items matching would be in this group and the order of the group would be the order of the elements array itself.

stormwarning avatar Dec 13 '24 19:12 stormwarning

Class components aren't used as much in React these days, so I understand if this isn't something to work on right away, I just wanted to point it out and see if there was a better way to configure this that I've missed

stormwarning avatar Dec 13 '24 19:12 stormwarning

@hugop95, I think https://github.com/azat-io/eslint-plugin-perfectionist/issues/408#issuecomment-2514187578 would address the need

OlivierZal avatar Dec 13 '24 19:12 OlivierZal

@OlivierZal @stormwarning

I'm actually not sure if this falls under the same need as what we discussed in #408: if I understand correctly:

  • The config works and sorts with the expected order.
  • But it's verbose.

Ideally, I could define a single react-lifecycle custom group and define the exact order within it

What is the objective behind this? Let's consider that you have

{
  groupName: 'react-lifecycle',
  elements: [
    "componentWillMount",
    "componentDidMount",
    // etc...
  ],
}

and

{
  groupName: 'other-stuff',
  elements: [
    "otherStuff1",
    "otherStuff2",
    // etc...
  ],
}

I assume you would want your groups to look like

groups: [
 "react-lifecycle",
 "other-stuff"
]

Is this better than doing

groups: [
 "componentWillMount",
 "componentDidMount",
 "otherStuff1",
 "otherStuff2",
]

?

Can you confirm that this is solely for convenience (and not a missing feature) purpose?

elements (or what have you) is a list of patterns. Items matching would be in this group and the order of the group would be the order of the elements array itself.

This is regrouping two things in one:

  • The order of groups (the order of the elements array).
  • What each group matches (each elements member).

I see two potential issues:

  • It's opinionated: choosing to match by element pattern is common, but is only one of all the filters that are offered.
  • customGroups order matters: the first custom group that matches is the one that will be used. Following your architecture, the following configuration can not explicitly tell where some elements would go:
[
	"component",
    "comp.*",
    "com.*",
]

Where should component go? (it matches the 3 members of the array).

hugop95 avatar Dec 13 '24 22:12 hugop95

The current configuration system already supports this use case with the verbose config shown in the original post. While the proposed enhancement would be more convenient, it introduces architectural complexity with pattern conflicts and mixed responsibilities.

Given that class components are less commonly used today and there's a working solution available, closing this as the existing functionality covers the need.

azat-io avatar Jun 17 '25 22:06 azat-io