From baa701d494baa8d01f977a650080fb52c61dbc46 Mon Sep 17 00:00:00 2001 From: Hugo <60015232+hugop95@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:23:45 +0200 Subject: [PATCH] feat: use dynamic group generation in sort-classes --- .gitignore | 1 + docs/content/rules/sort-classes.mdx | 229 ++++-- eslint.config.js | 2 +- rules/sort-classes-utils.ts | 97 +++ rules/sort-classes.ts | 268 +++++-- test/sort-classes-utils.test.ts | 47 ++ test/sort-classes.test.ts | 1144 ++++++++++++++++++++++++++- utils/use-groups.ts | 4 +- 8 files changed, 1618 insertions(+), 174 deletions(-) create mode 100644 rules/sort-classes-utils.ts create mode 100644 test/sort-classes-utils.test.ts diff --git a/.gitignore b/.gitignore index 952ab7664..4b566a51b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist/ # Editor .vscode/ .vim/ +.idea/ # Astro .astro/ diff --git a/docs/content/rules/sort-classes.mdx b/docs/content/rules/sort-classes.mdx index 011d71ed6..fb7ea7fe9 100644 --- a/docs/content/rules/sort-classes.mdx +++ b/docs/content/rules/sort-classes.mdx @@ -215,146 +215,215 @@ Allows you to use comments to separate the class members into logical groups. Th Allows you to specify a list of class member groups for sorting. Groups help organize class members into categories, prioritizing them during sorting. Multiple groups can be combined to achieve the desired sorting order. -There are a lot of predefined groups. - -Predefined Groups: - -- `'index-signature'` — Index signatures, which define the types of keys and values in an object. -- `'protected-decorated-accessor-property'` — Protected accessor properties with decorators. -- `'private-decorated-accessor-property'` — Private accessor properties with decorators. -- `'decorated-accessor-property'` — Accessor properties with decorators. -- `'protected-decorated-property'` — Protected properties with decorators. -- `'private-decorated-property'` — Private properties with decorators. -- `'decorated-property'` — Properties with decorators. -- `'protected-property'` — Protected properties. -- `'private-property'` — Private properties. -- `'static-property'` — Static properties. -- `'property'` — Regular properties. -- `'constructor'` — Constructor method. -- `'protected-method'` — Protected methods. -- `'private-method'` — Private methods. -- `'static-protected-method'` — Static protected methods. -- `'static-private-method'` — Static private methods. -- `'static-method'` — Static methods. -- `'decorated-method'` — Methods with decorators. -- `'decorated-get-method'` — Getter methods with decorators. -- `'decorated-set-method'` — Setter methods with decorators. -- `'get-method'` — Getter methods. -- `'set-method'` — Setter methods. -- `'method'` — Regular methods. -- `'unknown'` — Members that don’t fit into any other group. +Predefined groups are characterized by a single selector and potentially multiple modifiers. You may enter modifiers in any order, but the selector must always come at the end. -Example: +#### Constructors +- Selector: `constructor`. +- Modifiers: `protected`, `private`, `public`. +- Example: `protected-constructor`, `private-constructor`, `public-constructor` or `constructor`. + +#### Methods and accessors +- Method selectors: `get-method`, `set-method`, `method`. +- Accessors selector: `accessor-property`. +- Modifiers: `static`, `abstract`, `decorated`, `override`, `protected`, `private`, `public`. +- Example: `private-static-accessor-property`, `protected-abstract-override-method` or `static-get-method`. + +The `abstract` modifier is incompatible with the `static`, `private` and `decorated` modifiers. +`constructor`, `get-method` and `set-method` elements will also be matched as `method`. + +#### Properties +- Selector: `property`. +- Modifiers: `static`, `declare`, `abstract`, `decorated`, `override`, `readonly`, `protected`, `private`, `public`. +- Example: `readonly-decorated-property`. + +The `abstract` modifier is incompatible with the `static`, `private` and `decorated` modifiers. +The `declare` modifier is incompatible with the `override` and `decorated` modifiers. + +#### Index-signatures +- Selector: `index-signature`. +- Modifiers: `static`, `readonly`. +- Example: `static-readonly-index-signature`. + +#### Static-blocks +- Selector: `static-block`. +- Modifiers: No modifier available. +- Example: `static-block`. + +#### Important notes + +##### Scope of the `private` modifier +The `private` modifier will currently match any of the following: +- Elements with the `private` keyword. +- Elements with their name starting with `#`. + +##### Scope of the `public` modifier +Elements that are not `protected` nor `private` will be matched with the `public` modifier, even if the keyword is not present. + + +##### The `unknown` group +Members that don’t fit into any group entered by the user will be placed in the `unknown` group. + +##### Behavior when multiple groups match an element + +The lists of selectors and modifiers above are both sorted by importance, from most to least important. +In case of multiple groups matching an element, the following rules will be applied: + +1. Selector priority: `constructor`, `get-method` and `set-method` groups will always take precedence over `method` groups. +2. If the selector is the same, the group with the most modifiers matching will be selected. +3. If modifiers quantity is the same, order will be chosen based on modifier importance as listed above. + +Example 1: +```ts +abstract class Class { + + protected abstract get field(); + +} +``` + +`field` can be matched by the following groups, from most to least important: +- `abstract-protected-get-method` or `protected-abstract-get-method`. +- `abstract-get-method`. +- `protected-get-method`. +- `get-method`. +- `abstract-protected-method` or `protected-abstract-method`. +- `abstract-method`. +- `protected-method`. +- `method`. +- `unknown`. + +Example 2: (The most important group is written in the comments) ```ts -class Example { +abstract class Example extends BaseExample { + // 'index-signature' [key: string]: any; - // 'protected-decorated-accessor-property' - @SomeDecorator - protected get accessor() { - return this._value; - } + // 'public-static-property' + static instance: Example; - // 'private-decorated-accessor-property' - @SomeDecorator - private get accessor() { - return this._value; - } + // 'declare-protected-static-readonly-property' + declare protected static readonly value: string; - // 'decorated-accessor-property' + // 'protected-abstract-override-readonly-decorated-property' @SomeDecorator - get accessor() { - return this._value; - } + protected abstract override readonly _value: number; - // 'protected-decorated-property' - @SomeDecorator - protected _value: number; + // 'protected-property' + protected name: string; // 'private-decorated-property' @SomeDecorator private _value: number; - // 'decorated-property' - @SomeDecorator - public value: number; - - // 'protected-property' - protected name: string; - // 'private-property' private name: string; - // 'static-property' - static instance: Example; - - // 'property' + // 'public-property' public description: string; - // 'constructor' + // 'public-decorated-property' + @SomeDecorator + public value: number; + + // 'public-constructor' constructor(value: number) { this._value = value; } - // 'protected-method' - protected calculate() { - return this._value * 2; - } - - // 'private-method' - private calculate() { - return this._value * 2; + // 'public-static-method' + static getInstance() { + return this.instance; } - // 'static-protected-method' + // 'protected-static-method' protected static initialize() { this.instance = new Example(0); } - // 'static-private-method' + // 'private-static-method' private static initialize() { this.instance = new Example(0); } - // 'static-method' - static getInstance() { - return this.instance; + // 'protected-method' + protected calculate() { + return this._value * 2; + } + + // 'private-method' + private calculate() { + return this._value * 2; } - // 'decorated-method' + // 'public-decorated-method' @SomeDecorator public decoratedMethod() { return this._value; } - // 'decorated-get-method' + // 'public-method' + public display() { + console.log(this._value); + } + + // 'public-decorated-get-method' @SomeDecorator get decoratedValue() { return this._value; } - // 'decorated-set-method' + // 'public-decorated-set-method' @SomeDecorator set decoratedValue(value: number) { this._value = value; } - // 'get-method' + // 'protected-decorated-get-method' + @SomeDecorator + protected get value() { + return this._value; + } + + // 'private-decorated-get-method' + @SomeDecorator + private get value() { + return this._value; + } + + // 'public-decorated-get-method' + @SomeDecorator + get value() { + return this._value; + } + + // 'public-get-method' get value() { return this._value; } - // 'set-method' + // 'public-set-method' set value(value: number) { this._value = value; } - // 'method' - public display() { - console.log(this._value); + // 'protected-decorated-accessor-property' + @SomeDecorator + protected accessor _value: number; + + // 'private-decorated-accessor-property' + @SomeDecorator + private accessor _value: number; + + // 'static-block' + static { + console.log("I am a static block"); } + + // 'public-decorated-accessor-property' + @SomeDecorator + public accessor value: number; } ``` diff --git a/eslint.config.js b/eslint.config.js index fcb3ff04d..e98792434 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,6 +22,6 @@ module.exports = [ rules: { 'perfectionist/sort-objects': 'off', }, - files: ['**/test/*', '**/rules/*'], + files: ['**/test/**', '**/rules/**'], }, ] diff --git a/rules/sort-classes-utils.ts b/rules/sort-classes-utils.ts new file mode 100644 index 000000000..272f3edef --- /dev/null +++ b/rules/sort-classes-utils.ts @@ -0,0 +1,97 @@ +import type { Modifier, Selector } from './sort-classes' + +/** + * Cache computed groups by modifiers and selectors for performance + */ +const cachedGroupsByModifiersAndSelectors = new Map() + +/** + * Generates an ordered list of groups associated to modifiers and selectors entered + * The groups are generated by combining all possible combinations of modifiers with one selector in the end + * Selectors are prioritized over modifiers quantity. This means that + * `protected abstract override get fields();` should prioritize the 'get-method' group over the 'protected-abstract-override-method' group. + * @param modifiers List of modifiers associated to the selector, i.e ['abstract', 'protected'] + * @param selectors List of selectors, i.e ['get-method', 'method', 'property'] + */ +export const generateOfficialGroups = ( + modifiers: Modifier[], + selectors: Selector[], +): string[] => { + let modifiersAndSelectorsKey = modifiers.join('&') + '/' + selectors.join('&') + let cachedValue = cachedGroupsByModifiersAndSelectors.get( + modifiersAndSelectorsKey, + ) + if (cachedValue) { + return cachedValue + } + let allModifiersCombinations: string[][] = [] + for (let i = modifiers.length; i > 0; i--) { + allModifiersCombinations = [ + ...allModifiersCombinations, + ...getCombinations(modifiers, i), + ] + } + let allModifiersCombinationPermutations = allModifiersCombinations.flatMap( + result => getPermutations(result), + ) + let returnValue: string[] = [] + for (let selector of selectors) { + returnValue = [ + ...returnValue, + ...allModifiersCombinationPermutations.map( + modifiersCombinationPermutation => + [...modifiersCombinationPermutation, selector].join('-'), + ), + selector, + ] + } + cachedGroupsByModifiersAndSelectors.set(modifiersAndSelectorsKey, returnValue) + return returnValue +} + +/** + * Get possible combinations of n elements from an array + */ +const getCombinations = (array: string[], n: number): string[][] => { + let result: string[][] = [] + + let backtrack = (start: number, comb: string[]) => { + if (comb.length === n) { + result.push([...comb]) + return + } + for (let i = start; i < array.length; i++) { + comb.push(array[i]) + backtrack(i + 1, comb) + comb.pop() + } + } + + backtrack(0, []) + return result +} + +/** + * Get all permutations of an array + * This allows 'abstract-override-protected-get-method', 'override-protected-abstract-get-method', + * 'protected-abstract-override-get-method'... to be entered by the user and always match the same group + * This can theoretically cause performance issues in case users enter too many modifiers at once? 8 modifiers would result + * in 40320 permutations, 9 in 362880. + */ +const getPermutations = (elements: string[]): string[][] => { + let result: string[][] = [] + let backtrack = (first: number) => { + if (first === elements.length) { + result.push([...elements]) + return + } + for (let i = first; i < elements.length; i++) { + ;[elements[first], elements[i]] = [elements[i], elements[first]] + backtrack(first + 1) + ;[elements[first], elements[i]] = [elements[i], elements[first]] + } + } + backtrack(0) + + return result +} diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts index 4d283437b..5f9c7581e 100644 --- a/rules/sort-classes.ts +++ b/rules/sort-classes.ts @@ -6,6 +6,7 @@ import type { SortingNode } from '../typings' import { isPartitionComment } from '../utils/is-partition-comment' import { getCommentBefore } from '../utils/get-comment-before' import { createEslintRule } from '../utils/create-eslint-rule' +import { generateOfficialGroups } from './sort-classes-utils' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' import { toSingleLine } from '../utils/to-single-line' @@ -20,31 +21,94 @@ import { compare } from '../utils/compare' type MESSAGE_ID = 'unexpectedClassesOrder' +type ProtectedModifier = 'protected' +type PrivateModifier = 'private' +type PublicModifier = 'public' +type StaticModifier = 'static' +type AbstractModifier = 'abstract' +type OverrideModifier = 'override' +type ReadonlyModifier = 'readonly' +type DecoratedModifier = 'decorated' +type DeclareModifier = 'declare' +export type Modifier = + | ProtectedModifier + | DecoratedModifier + | AbstractModifier + | OverrideModifier + | ReadonlyModifier + | PrivateModifier + | DeclareModifier + | PublicModifier + | StaticModifier + +type ConstructorSelector = 'constructor' +type PropertySelector = 'property' +type MethodSelector = 'method' +type GetMethodSelector = 'get-method' +type SetMethodSelector = 'set-method' +type IndexSignatureSelector = 'index-signature' +type StaticBlockSelector = 'static-block' +type AccessorPropertySelector = 'accessor-property' +export type Selector = + | AccessorPropertySelector + | IndexSignatureSelector + | ConstructorSelector + | StaticBlockSelector + | GetMethodSelector + | SetMethodSelector + | PropertySelector + | MethodSelector + +type WithDashSuffixOrEmpty = `${T}-` | '' + +type PublicOrProtectedOrPrivateModifierPrefix = WithDashSuffixOrEmpty< + ProtectedModifier | PrivateModifier | PublicModifier +> + +type OverrideModifierPrefix = WithDashSuffixOrEmpty +type ReadonlyModifierPrefix = WithDashSuffixOrEmpty +type DecoratedModifierPrefix = WithDashSuffixOrEmpty +type DeclareModifierPrefix = WithDashSuffixOrEmpty + +type StaticOrAbstractModifierPrefix = WithDashSuffixOrEmpty< + AbstractModifier | StaticModifier +> + +type StaticModifierPrefix = WithDashSuffixOrEmpty + +type MethodOrGetMethodOrSetMethodSelector = + | GetMethodSelector + | SetMethodSelector + | MethodSelector + +type ConstructorGroup = + `${PublicOrProtectedOrPrivateModifierPrefix}${ConstructorSelector}` +type DeclarePropertyGroup = + `${DeclareModifierPrefix}${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${ReadonlyModifierPrefix}${PropertySelector}` +type NonDeclarePropertyGroup = + `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${ReadonlyModifierPrefix}${DecoratedModifierPrefix}${PropertySelector}` +type MethodOrGetMethodOrSetMethodGroup = + `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${DecoratedModifierPrefix}${MethodOrGetMethodOrSetMethodSelector}` +type AccessorPropertyGroup = + `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${DecoratedModifierPrefix}${AccessorPropertySelector}` +type IndexSignatureGroup = + `${StaticModifierPrefix}${ReadonlyModifierPrefix}${IndexSignatureSelector}` +type StaticBlockGroup = `${StaticBlockSelector}` + +/** + * Some invalid combinations are still handled by this type, such as + * - private abstract X + * - abstract decorated X + */ type Group = - | 'protected-decorated-accessor-property' - | 'private-decorated-accessor-property' - | 'protected-decorated-property' - | 'decorated-accessor-property' - | 'private-decorated-property' - | 'static-protected-method' - | 'static-private-method' - | 'decorated-set-method' - | 'decorated-get-method' - | 'decorated-property' - | 'protected-property' - | 'decorated-method' - | 'private-property' - | 'protected-method' - | 'static-property' - | 'index-signature' - | 'private-method' - | 'static-method' - | 'constructor' - | 'get-method' - | 'set-method' - | 'property' + | MethodOrGetMethodOrSetMethodGroup + | NonDeclarePropertyGroup + | AccessorPropertyGroup + | DeclarePropertyGroup + | IndexSignatureGroup + | ConstructorGroup + | StaticBlockGroup | 'unknown' - | 'method' | string type Options = [ @@ -278,105 +342,141 @@ export default createEslintRule({ } } - let isPrivate = name.startsWith('_') || name.startsWith('#') + let isPrivateName = name.startsWith('#') let decorated = 'decorators' in member && member.decorators.length > 0 - if (member.type === 'MethodDefinition') { - if (member.kind === 'constructor') { - defineGroup('constructor') + let modifiers: Modifier[] = [] + let selectors: Selector[] = [] + if ( + member.type === 'MethodDefinition' || + member.type === 'TSAbstractMethodDefinition' + ) { + // By putting the static modifier before accessibility modifiers, + // we prioritize 'static' over those in cases like: + // Config: ['static-method', 'public-method'] + // Element: public static method(); + // Element will be classified as 'static-method' before 'public-method' + if (member.static) { + modifiers.push('static') + } + if (member.type === 'TSAbstractMethodDefinition') { + modifiers.push('abstract') } - - let isProtectedMethod = member.accessibility === 'protected' - - let isPrivateMethod = - member.accessibility === 'private' || isPrivate - - let isStaticMethod = member.static if (decorated) { - if (member.kind === 'get') { - defineGroup('decorated-get-method') - } - - if (member.kind === 'set') { - defineGroup('decorated-set-method') - } + modifiers.push('decorated') + } - defineGroup('decorated-method') + if (member.override) { + modifiers.push('override') } - if (isPrivateMethod && isStaticMethod) { - defineGroup('static-private-method') + if (member.accessibility === 'protected') { + modifiers.push('protected') + } else if (member.accessibility === 'private' || isPrivateName) { + modifiers.push('private') + } else { + modifiers.push('public') } - if (isPrivateMethod) { - defineGroup('private-method') + if (member.kind === 'constructor') { + selectors.push('constructor') } - if (isStaticMethod) { - defineGroup('static-method') + if (member.kind === 'get') { + selectors.push('get-method') } - if (isProtectedMethod && isStaticMethod) { - defineGroup('static-protected-method') + if (member.kind === 'set') { + selectors.push('set-method') + } + selectors.push('method') + } else if (member.type === 'TSIndexSignature') { + if (member.static) { + modifiers.push('static') } - if (isProtectedMethod) { - defineGroup('protected-method') + if (member.readonly) { + modifiers.push('readonly') } - if (member.kind === 'get') { - defineGroup('get-method') + selectors.push('index-signature') + } else if (member.type === 'StaticBlock') { + selectors.push('static-block') + } else if ( + member.type === 'AccessorProperty' || + member.type === 'TSAbstractAccessorProperty' + ) { + if (member.static) { + modifiers.push('static') } - if (member.kind === 'set') { - defineGroup('set-method') + if (member.type === 'TSAbstractAccessorProperty') { + modifiers.push('abstract') } - defineGroup('method') - } else if (member.type === 'TSIndexSignature') { - defineGroup('index-signature') - } else if (member.type === 'AccessorProperty') { if (decorated) { - if (member.accessibility === 'protected') { - defineGroup('protected-decorated-accessor-property') - } + modifiers.push('decorated') + } - if (member.accessibility === 'private' || isPrivate) { - defineGroup('private-decorated-accessor-property') - } + if (member.override) { + modifiers.push('override') + } - defineGroup('decorated-accessor-property') + if (member.accessibility === 'protected') { + modifiers.push('protected') + } else if (member.accessibility === 'private' || isPrivateName) { + modifiers.push('private') + } else { + modifiers.push('public') } - } else if (member.type === 'PropertyDefinition') { - if (decorated) { - if (member.accessibility === 'protected') { - defineGroup('protected-decorated-property') - } + selectors.push('accessor-property') + } else { + // Member is necessarily a Property - if (member.accessibility === 'private' || isPrivate) { - defineGroup('private-decorated-property') - } + // Similarly to above for methods, prioritize 'static', 'declare', 'decorated', 'abstract', 'override' and 'readonly' + // over accessibility modifiers + if (member.static) { + modifiers.push('static') + } - defineGroup('decorated-property') + if (member.declare) { + modifiers.push('declare') } - if (member.accessibility === 'protected') { - defineGroup('protected-property') + if (member.type === 'TSAbstractPropertyDefinition') { + modifiers.push('abstract') } - if (member.accessibility === 'private' || isPrivate) { - defineGroup('private-property') + if (decorated) { + modifiers.push('decorated') } - if (member.static) { - defineGroup('static-property') + if (member.override) { + modifiers.push('override') } - defineGroup('property') - } + if (member.readonly) { + modifiers.push('readonly') + } + + if (member.accessibility === 'protected') { + modifiers.push('protected') + } else if (member.accessibility === 'private' || isPrivateName) { + modifiers.push('private') + } else { + modifiers.push('public') + } + selectors.push('property') + } + for (let officialGroup of generateOfficialGroups( + modifiers, + selectors, + )) { + defineGroup(officialGroup) + } setCustomGroups(options.customGroups, name, { override: true, }) diff --git a/test/sort-classes-utils.test.ts b/test/sort-classes-utils.test.ts new file mode 100644 index 000000000..e98fabf57 --- /dev/null +++ b/test/sort-classes-utils.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' + +import { generateOfficialGroups } from '../rules/sort-classes-utils' + +describe('sort-classes-utils', () => { + it('sort-classes-utils: should generate official groups', () => { + expect( + generateOfficialGroups( + ['protected', 'abstract', 'override'], + ['get-method', 'method'], + ), + ).toEqual([ + 'protected-abstract-override-get-method', + 'protected-override-abstract-get-method', + 'abstract-protected-override-get-method', + 'abstract-override-protected-get-method', + 'override-abstract-protected-get-method', + 'override-protected-abstract-get-method', + 'protected-abstract-get-method', + 'abstract-protected-get-method', + 'protected-override-get-method', + 'override-protected-get-method', + 'abstract-override-get-method', + 'override-abstract-get-method', + 'protected-get-method', + 'abstract-get-method', + 'override-get-method', + 'get-method', + 'protected-abstract-override-method', + 'protected-override-abstract-method', + 'abstract-protected-override-method', + 'abstract-override-protected-method', + 'override-abstract-protected-method', + 'override-protected-abstract-method', + 'protected-abstract-method', + 'abstract-protected-method', + 'protected-override-method', + 'override-protected-method', + 'abstract-override-method', + 'override-abstract-method', + 'protected-method', + 'abstract-method', + 'override-method', + 'method', + ]) + }) +}) diff --git a/test/sort-classes.test.ts b/test/sort-classes.test.ts index 941a99b1e..5d3193b1f 100644 --- a/test/sort-classes.test.ts +++ b/test/sort-classes.test.ts @@ -169,6 +169,1113 @@ describe(ruleName, () => { ], }) + ruleTester.run( + `${ruleName}(${type}): sorts complex official groups`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + abstract class Class { + + static {} + + static readonly [key: string]: string; + + declare private static readonly l; + + private k = 'k'; + + protected j = 'j'; + + public i = 'i'; + + private readonly h = 'h'; + + protected readonly g = 'g'; + + public readonly f = 'f'; + + private static override readonly e = 'e'; + + protected static override readonly d = 'd'; + + static override readonly c = 'c'; + + @Decorator + protected abstract override readonly b; + + @Decorator + abstract override readonly a; + } + `, + output: dedent` + abstract class Class { + + @Decorator + abstract override readonly a; + + @Decorator + protected abstract override readonly b; + + static override readonly c = 'c'; + + protected static override readonly d = 'd'; + + private static override readonly e = 'e'; + + public readonly f = 'f'; + + protected readonly g = 'g'; + + private readonly h = 'h'; + + public i = 'i'; + + protected j = 'j'; + + private k = 'k'; + + declare private static readonly l; + + static readonly [key: string]: string; + + static {} + } + `, + options: [ + { + ...options, + groups: [ + 'unknown', + 'public-abstract-override-readonly-decorated-property', + 'protected-abstract-override-readonly-decorated-property', + 'static-public-override-readonly-property', + 'static-protected-override-readonly-property', + 'static-private-override-readonly-property', + 'public-readonly-property', + 'protected-readonly-property', + 'private-readonly-property', + 'public-property', + 'protected-property', + 'private-property', + 'declare-private-static-readonly-property', + 'static-readonly-index-signature', + 'static-block', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'static', + right: 'static readonly [key: string]', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'static readonly [key: string]', + right: 'l', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'l', + right: 'k', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'k', + right: 'j', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'j', + right: 'i', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'i', + right: 'h', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'h', + right: 'g', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'g', + right: 'f', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'f', + right: 'e', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'e', + right: 'd', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'd', + right: 'c', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'c', + right: 'b', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize selectors over modifiers quantity`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + public abstract override method(): string; + + public abstract override get fields(): string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + public abstract override get fields(): string; + + public abstract override method(): string; + } + `, + options: [ + { + ...options, + groups: ['get-method', 'public-abstract-override-method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'fields', + }, + }, + ], + }, + ], + }, + ) + + describe('index-signature modifiers priority', () => { + ruleTester.run( + `${ruleName}(${type}): prioritize static over readonly`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a: string; + + static readonly [key: string]: string; + } + `, + output: dedent` + export class Class { + + static readonly [key: string]: string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'static-index-signature', + 'property', + 'readonly-index-signature', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'static readonly [key: string]', + }, + }, + ], + }, + ], + }, + ) + }) + + describe('method selectors priority', () => { + ruleTester.run( + `${ruleName}(${type}): prioritize constructor over method`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a(): void; + + constructor() {} + } + `, + output: dedent` + export class Class { + + constructor() {} + + a(): void; + } + `, + options: [ + { + ...options, + groups: ['constructor', 'method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'constructor', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize get-method over method`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a(): void; + + get z() {} + } + `, + output: dedent` + export class Class { + + get z() {} + + a(): void; + } + `, + options: [ + { + ...options, + groups: ['get-method', 'method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize set-method over method`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a(): void; + + set z() {} + } + `, + output: dedent` + export class Class { + + set z() {} + + a(): void; + } + `, + options: [ + { + ...options, + groups: ['set-method', 'method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + }) + + describe('method modifiers priority', () => { + ruleTester.run( + `${ruleName}(${type}): prioritize static over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class extends Class2 { + + a: string; + + static override z(): string; + } + `, + output: dedent` + export class Class extends Class2 { + + static override z(): string; + + a: string; + } + `, + options: [ + { + ...options, + groups: ['static-method', 'property', 'override-method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize abstract over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a: string; + + abstract override z(): string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + abstract override z(): string; + + a: string; + } + `, + options: [ + { + ...options, + groups: ['abstract-method', 'property', 'override-method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize decorated over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a: string; + + @Decorator + override z(): void {} + } + `, + output: dedent` + export abstract class Class extends Class2 { + + @Decorator + override z(): void {} + + a: string; + } + `, + options: [ + { + ...options, + groups: ['decorated-method', 'property', 'override-method'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + for (let accessibilityModifier of ['public', 'protected', 'private']) { + ruleTester.run( + `${ruleName}(${type}): prioritize override over ${accessibilityModifier} accessibility`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a: string; + + ${accessibilityModifier} override z(): string; + } + `, + output: dedent` + export class Class { + + ${accessibilityModifier} override z(): string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'override-method', + 'property', + `${accessibilityModifier}-method`, + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + } + }) + + describe('accessor modifiers priority', () => { + ruleTester.run( + `${ruleName}(${type}): prioritize static over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class extends Class2 { + + a: string; + + static override accessor z: string; + } + `, + output: dedent` + export class Class extends Class2 { + + static override accessor z: string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'static-accessor-property', + 'property', + 'override-accessor-property', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize abstract over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a: string; + + abstract override accessor z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + abstract override accessor z: string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'abstract-accessor-property', + 'property', + 'override-accessor-property', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize decorated over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a: string; + + @Decorator + override accessor z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + @Decorator + override accessor z: string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'decorated-accessor-property', + 'property', + 'override-accessor-property', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + for (let accessibilityModifier of ['public', 'protected', 'private']) { + ruleTester.run( + `${ruleName}(${type}): prioritize override over ${accessibilityModifier} accessibility`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a: string; + + ${accessibilityModifier} override accessor z: string; + } + `, + output: dedent` + export class Class { + + ${accessibilityModifier} override accessor z: string; + + a: string; + } + `, + options: [ + { + ...options, + groups: [ + 'override-accessor-property', + 'property', + `${accessibilityModifier}-accessor-property`, + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + } + }) + + describe('property modifiers priority', () => { + ruleTester.run( + `${ruleName}(${type}): prioritize static over declare`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class extends Class2 { + + a(): void {} + + declare static z: string; + } + `, + output: dedent` + export class Class extends Class2 { + + declare static z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['static-property', 'method', 'declare-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize declare over abstract`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class extends Class2 { + + a(): void {} + + declare abstract z: string; + } + `, + output: dedent` + export class Class extends Class2 { + + declare abstract z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['declare-property', 'method', 'abstract-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize abstract over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a(): void {} + + abstract override z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + abstract override z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['abstract-property', 'method', 'override-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize decorated over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a(): void {} + + @Decorator + override z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + @Decorator + override z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['decorated-property', 'method', 'override-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize decorated over override`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a(): void {} + + @Decorator + override z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + @Decorator + override z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['decorated-property', 'method', 'override-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize override over readonly`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export abstract class Class extends Class2 { + + a(): void {} + + override readonly z: string; + } + `, + output: dedent` + export abstract class Class extends Class2 { + + override readonly z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: ['override-property', 'method', 'readonly-property'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + + for (let accessibilityModifier of ['public', 'protected', 'private']) { + ruleTester.run( + `${ruleName}(${type}): prioritize readonly over ${accessibilityModifier} accessibility`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + export class Class { + + a(): void {} + + ${accessibilityModifier} readonly z: string; + } + `, + output: dedent` + export class Class { + + ${accessibilityModifier} readonly z: string; + + a(): void {} + } + `, + options: [ + { + ...options, + groups: [ + 'readonly-property', + 'method', + `${accessibilityModifier}-property`, + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'a', + right: 'z', + }, + }, + ], + }, + ], + }, + ) + } + }) + ruleTester.run( `${ruleName}(${type}): sorts class and group members`, rule, @@ -395,14 +1502,14 @@ describe(ruleName, () => { `, output: dedent` class MyUnsortedClass { + static #someStaticPrivateProperty = 4 + static someStaticProperty = 3 #someOtherPrivateProperty = 2 #somePrivateProperty - static #someStaticPrivateProperty = 4 - someOtherProperty someProperty = 1 @@ -455,6 +1562,13 @@ describe(ruleName, () => { right: 'someStaticProperty', }, }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'someStaticProperty', + right: '#someStaticPrivateProperty', + }, + }, { messageId: 'unexpectedClassesOrder', data: { @@ -1736,14 +2850,14 @@ describe(ruleName, () => { `, output: dedent` class MyUnsortedClass { + static #someStaticPrivateProperty = 4 + static someStaticProperty = 3 #someOtherPrivateProperty = 2 #somePrivateProperty - static #someStaticPrivateProperty = 4 - someOtherProperty someProperty = 1 @@ -1796,6 +2910,13 @@ describe(ruleName, () => { right: 'someStaticProperty', }, }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'someStaticProperty', + right: '#someStaticPrivateProperty', + }, + }, { messageId: 'unexpectedClassesOrder', data: { @@ -3046,10 +4167,10 @@ describe(ruleName, () => { `, output: dedent` class MyUnsortedClass { - static someStaticProperty = 3 - static #someStaticPrivateProperty = 4 + static someStaticProperty = 3 + #someOtherPrivateProperty = 2 #somePrivateProperty @@ -3062,10 +4183,10 @@ describe(ruleName, () => { aInstanceMethod () {} - static #aPrivateStaticMethod () {} - #aPrivateInstanceMethod () {} + static #aPrivateStaticMethod () {} + static aStaticMethod () {} } `, @@ -3106,6 +4227,13 @@ describe(ruleName, () => { right: 'someStaticProperty', }, }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'someStaticProperty', + right: '#someStaticPrivateProperty', + }, + }, { messageId: 'unexpectedClassesOrder', data: { diff --git a/utils/use-groups.ts b/utils/use-groups.ts index ed1eecc28..040eb3b26 100644 --- a/utils/use-groups.ts +++ b/utils/use-groups.ts @@ -2,9 +2,11 @@ import { minimatch } from 'minimatch' export let useGroups = (groups: (string[] | string)[]) => { let group: undefined | string + // For lookup performance + let groupsSet = new Set(groups.flat()) let defineGroup = (value: string, override = false) => { - if ((!group || override) && groups.flat().includes(value)) { + if ((!group || override) && groupsSet.has(value)) { group = value } }