diff --git a/docs/content/rules/sort-classes.mdx b/docs/content/rules/sort-classes.mdx index a7a450b92..7dc03378e 100644 --- a/docs/content/rules/sort-classes.mdx +++ b/docs/content/rules/sort-classes.mdx @@ -266,7 +266,7 @@ Specifies how new lines should be handled between class member groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in object types. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### ignoreCallbackDependenciesPatterns diff --git a/docs/content/rules/sort-imports.mdx b/docs/content/rules/sort-imports.mdx index 8ae738bf0..3ee465067 100644 --- a/docs/content/rules/sort-imports.mdx +++ b/docs/content/rules/sort-imports.mdx @@ -227,7 +227,11 @@ Specifies how new lines should be handled between import groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in the entire import section. -This options is only applicable when `partitionByNewLine` is `false`. +You can also enforce the newline behavior between two specific groups through the `groups` options. + +See the [`groups`](#newlines-between-groups) option. + +This option is only applicable when `partitionByNewLine` is `false`. ### maxLineLength @@ -333,6 +337,25 @@ import type { Details } from './data' import type { BaseOptions } from './index.d.ts' ``` +#### Newlines between groups + +You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups. + +See the [`newlinesBetween`](#newlinesbetween) option. + +This feature is only applicable when `partitionByNewLine` is false. + +```ts +{ + newlinesBetween: 'always', + groups: [ + 'a', + { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option + 'b', + ] +} +``` + ### customGroups diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx index 5a31bd787..2fbe08dd5 100644 --- a/docs/content/rules/sort-interfaces.mdx +++ b/docs/content/rules/sort-interfaces.mdx @@ -248,7 +248,7 @@ Specifies how new lines should be handled between interface groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed between interface members. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### [DEPRECATED] groupKind diff --git a/docs/content/rules/sort-intersection-types.mdx b/docs/content/rules/sort-intersection-types.mdx index 396d56381..ae214ff62 100644 --- a/docs/content/rules/sort-intersection-types.mdx +++ b/docs/content/rules/sort-intersection-types.mdx @@ -176,7 +176,7 @@ Specifies how new lines should be handled between intersection type groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in intersection types. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### groups diff --git a/docs/content/rules/sort-modules.mdx b/docs/content/rules/sort-modules.mdx index 246c3d4a8..aee5345e6 100644 --- a/docs/content/rules/sort-modules.mdx +++ b/docs/content/rules/sort-modules.mdx @@ -290,7 +290,7 @@ Specifies how new lines should be handled between module member groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in object types. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### groups diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx index 3dce69cad..b90dabd42 100644 --- a/docs/content/rules/sort-object-types.mdx +++ b/docs/content/rules/sort-object-types.mdx @@ -213,7 +213,7 @@ Specifies how new lines should be handled between object type groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in object types. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### [DEPRECATED] groupKind diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx index 953a1dbf6..7165f45bf 100644 --- a/docs/content/rules/sort-objects.mdx +++ b/docs/content/rules/sort-objects.mdx @@ -258,7 +258,7 @@ Specifies how new lines should be handled between object groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in objects. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### styledComponents diff --git a/docs/content/rules/sort-union-types.mdx b/docs/content/rules/sort-union-types.mdx index ff22df3aa..100f40662 100644 --- a/docs/content/rules/sort-union-types.mdx +++ b/docs/content/rules/sort-union-types.mdx @@ -196,7 +196,7 @@ Specifies how new lines should be handled between union type groups. - `always` — Enforce one new line between each group, and forbid new lines inside a group. - `never` — No new lines are allowed in union types. -This options is only applicable when `partitionByNewLine` is `false`. +This option is only applicable when `partitionByNewLine` is `false`. ### groups diff --git a/rules/sort-classes/types.ts b/rules/sort-classes/types.ts index 386b50d64..c83a8b68d 100644 --- a/rules/sort-classes/types.ts +++ b/rules/sort-classes/types.ts @@ -18,6 +18,11 @@ export type SortClassesOptions = [ | string[] | boolean | string + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -25,7 +30,6 @@ export type SortClassesOptions = [ locales: NonNullable partitionByNewLine: boolean customGroups: CustomGroup[] - groups: (Group[] | Group)[] order: 'desc' | 'asc' ignoreCase: boolean alphabet: string diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index 264893a5f..a9eaf163e 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -57,11 +57,15 @@ export type Options = [ value?: Record type?: Record } + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (Group[] | Group)[] environment: 'node' | 'bun' partitionByNewLine: boolean internalPattern: string[] @@ -177,9 +181,13 @@ export default createEslintRule, MESSAGE_ID>({ : null let isSideEffectOnlyGroup = ( - group: undefined | string[] | string, + group: + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | undefined + | string[] + | string, ): boolean => { - if (!group) { + if (!group || (typeof group === 'object' && 'newlinesBetween' in group)) { return false } if (typeof group === 'string') { diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts index 8b28e7af3..5bd14e3d5 100644 --- a/rules/sort-intersection-types.ts +++ b/rules/sort-intersection-types.ts @@ -45,11 +45,15 @@ type Options = [ | string[] | boolean | string + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (Group[] | Group)[] partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean diff --git a/rules/sort-modules/types.ts b/rules/sort-modules/types.ts index 2d01cdc7d..532d4a4e5 100644 --- a/rules/sort-modules/types.ts +++ b/rules/sort-modules/types.ts @@ -18,12 +18,16 @@ export type SortModulesOptions = [ | string[] | boolean | string + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable customGroups: CustomGroup[] - groups: (Group[] | Group)[] partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean diff --git a/rules/sort-object-types/types.ts b/rules/sort-object-types/types.ts index 9f1c7084e..37cbf670e 100644 --- a/rules/sort-object-types/types.ts +++ b/rules/sort-object-types/types.ts @@ -21,6 +21,11 @@ export type Options = Partial<{ declarationMatchesPattern?: string allNamesMatchPattern?: string } + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'unsorted' | 'natural' | 'custom' customGroups: Record | CustomGroup[] /** @@ -30,7 +35,6 @@ export type Options = Partial<{ newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (Group[] | Group)[] partitionByNewLine: boolean /** * @deprecated for {@link `useConfigurationIf.declarationMatchesPattern`} diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts index 177963985..84065eab1 100644 --- a/rules/sort-objects.ts +++ b/rules/sort-objects.ts @@ -60,13 +60,17 @@ type Options = Partial<{ callingFunctionNamePattern?: string allNamesMatchPattern?: string } + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'unsorted' | 'natural' | 'custom' destructuredObjects: { groups: boolean } | boolean customGroups: Record newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (Group[] | Group)[] partitionByNewLine: boolean objectDeclarations: boolean styledComponents: boolean diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts index fd4e35c46..39514f179 100644 --- a/rules/sort-union-types.ts +++ b/rules/sort-union-types.ts @@ -45,11 +45,15 @@ type Options = [ | string[] | boolean | string + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | Group[] + | Group + )[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (Group[] | Group)[] partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean diff --git a/test/rules/sort-classes.test.ts b/test/rules/sort-classes.test.ts index f619ece9c..387900b31 100644 --- a/test/rules/sort-classes.test.ts +++ b/test/rules/sort-classes.test.ts @@ -4427,6 +4427,92 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + customGroups: [ + { elementNamePattern: 'a', groupName: 'a' }, + { elementNamePattern: 'b', groupName: 'b' }, + { elementNamePattern: 'c', groupName: 'c' }, + { elementNamePattern: 'd', groupName: 'd' }, + { elementNamePattern: 'e', groupName: 'e' }, + ], + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenClassMembers', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenClassMembers', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenClassMembers', + }, + ], + output: dedent` + class Class { + a: string + + b: string + + c: string + d: string + + + e: string + } + `, + code: dedent` + class Class { + a: string + b: string + + + c: string + + d: string + + + e: string + } + `, + }, + ], + valid: [], + }, + ) }) describe(`${ruleName}(${type}): sorts inline elements correctly`, () => { diff --git a/test/rules/sort-imports.test.ts b/test/rules/sort-imports.test.ts index d09cd873e..65c77f89f 100644 --- a/test/rules/sort-imports.test.ts +++ b/test/rules/sort-imports.test.ts @@ -2031,6 +2031,90 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + customGroups: { + value: { + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + }, + }, + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenImports', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenImports', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenImports', + }, + ], + output: dedent` + import { A } from 'a' + + import { B } from 'b' + + import { C } from 'c' + import { D } from 'd' + + + import { E } from 'e' + `, + code: dedent` + import { A } from 'a' + import { B } from 'b' + + + import { C } from 'c' + + import { D } from 'd' + + + import { E } from 'e' + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/rules/sort-interfaces.test.ts b/test/rules/sort-interfaces.test.ts index d1ff6f365..363b0cff7 100644 --- a/test/rules/sort-interfaces.test.ts +++ b/test/rules/sort-interfaces.test.ts @@ -2304,6 +2304,92 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + customGroups: [ + { elementNamePattern: 'a', groupName: 'a' }, + { elementNamePattern: 'b', groupName: 'b' }, + { elementNamePattern: 'c', groupName: 'c' }, + { elementNamePattern: 'd', groupName: 'd' }, + { elementNamePattern: 'e', groupName: 'e' }, + ], + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenInterfaceMembers', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenInterfaceMembers', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenInterfaceMembers', + }, + ], + output: dedent` + interface Interface { + a: string + + b: string + + c: string + d: string + + + e: string + } + `, + code: dedent` + interface Interface { + a: string + b: string + + + c: string + + d: string + + + e: string + } + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/rules/sort-intersection-types.test.ts b/test/rules/sort-intersection-types.test.ts index 7702953e5..24877ef85 100644 --- a/test/rules/sort-intersection-types.test.ts +++ b/test/rules/sort-intersection-types.test.ts @@ -1121,6 +1121,83 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: '{ a: string }', + left: '() => void', + }, + messageId: 'missedSpacingBetweenIntersectionTypes', + }, + { + data: { + left: '{ a: string }', + right: 'A', + }, + messageId: 'extraSpacingBetweenIntersectionTypes', + }, + { + data: { + right: '[A]', + left: 'A', + }, + messageId: 'extraSpacingBetweenIntersectionTypes', + }, + ], + options: [ + { + ...options, + groups: [ + 'function', + { newlinesBetween: 'always' }, + 'object', + { newlinesBetween: 'always' }, + 'named', + { newlinesBetween: 'never' }, + 'tuple', + { newlinesBetween: 'ignore' }, + 'nullish', + ], + newlinesBetween: 'always', + }, + ], + output: dedent` + type Type = + (() => void) & + + { a: string } & + + A & + [A] & + + + null + `, + code: dedent` + type Type = + (() => void) & + { a: string } & + + + A & + + [A] & + + + null + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/rules/sort-modules.test.ts b/test/rules/sort-modules.test.ts index 9b8db8721..237d322fd 100644 --- a/test/rules/sort-modules.test.ts +++ b/test/rules/sort-modules.test.ts @@ -2105,6 +2105,88 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + customGroups: [ + { elementNamePattern: 'a', groupName: 'a' }, + { elementNamePattern: 'b', groupName: 'b' }, + { elementNamePattern: 'c', groupName: 'c' }, + { elementNamePattern: 'd', groupName: 'd' }, + { elementNamePattern: 'e', groupName: 'e' }, + ], + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenModulesMembers', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenModulesMembers', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenModulesMembers', + }, + ], + output: dedent` + function a() {} + + function b() {} + + function c() {} + function d() {} + + + function e() {} + `, + code: dedent` + function a() {} + function b() {} + + + function c() {} + + function d() {} + + + function e() {} + `, + }, + ], + valid: [], + }, + ) }) describe(`${ruleName}(${type}): sorts inline elements correctly`, () => { diff --git a/test/rules/sort-object-types.test.ts b/test/rules/sort-object-types.test.ts index 6c6e84e4b..04b6a990b 100644 --- a/test/rules/sort-object-types.test.ts +++ b/test/rules/sort-object-types.test.ts @@ -2100,6 +2100,92 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + customGroups: [ + { elementNamePattern: 'a', groupName: 'a' }, + { elementNamePattern: 'b', groupName: 'b' }, + { elementNamePattern: 'c', groupName: 'c' }, + { elementNamePattern: 'd', groupName: 'd' }, + { elementNamePattern: 'e', groupName: 'e' }, + ], + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenObjectTypeMembers', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenObjectTypeMembers', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenObjectTypeMembers', + }, + ], + output: dedent` + type Type = { + a: string + + b: string + + c: string + d: string + + + e: string + } + `, + code: dedent` + type Type = { + a: string + b: string + + + c: string + + d: string + + + e: string + } + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/rules/sort-objects.test.ts b/test/rules/sort-objects.test.ts index 18710aedb..e0e270edc 100644 --- a/test/rules/sort-objects.test.ts +++ b/test/rules/sort-objects.test.ts @@ -2044,6 +2044,92 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + groups: [ + 'a', + { newlinesBetween: 'always' }, + 'b', + { newlinesBetween: 'always' }, + 'c', + { newlinesBetween: 'never' }, + 'd', + { newlinesBetween: 'ignore' }, + 'e', + ], + customGroups: { + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + }, + newlinesBetween: 'always', + }, + ], + errors: [ + { + data: { + right: 'b', + left: 'a', + }, + messageId: 'missedSpacingBetweenObjectMembers', + }, + { + data: { + right: 'c', + left: 'b', + }, + messageId: 'extraSpacingBetweenObjectMembers', + }, + { + data: { + right: 'd', + left: 'c', + }, + messageId: 'extraSpacingBetweenObjectMembers', + }, + ], + output: dedent` + let obj = { + a: 'a', + + b: 'b', + + c: 'c', + d: 'd', + + + e: 'e', + } + `, + code: dedent` + let obj = { + a: 'a', + b: 'b', + + + c: 'c', + + d: 'd', + + + e: 'e', + } + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/rules/sort-union-types.test.ts b/test/rules/sort-union-types.test.ts index fd058cb19..39cb313c2 100644 --- a/test/rules/sort-union-types.test.ts +++ b/test/rules/sort-union-types.test.ts @@ -1124,6 +1124,83 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: '{ a: string }', + left: '() => void', + }, + messageId: 'missedSpacingBetweenUnionTypes', + }, + { + data: { + left: '{ a: string }', + right: 'A', + }, + messageId: 'extraSpacingBetweenUnionTypes', + }, + { + data: { + right: '[A]', + left: 'A', + }, + messageId: 'extraSpacingBetweenUnionTypes', + }, + ], + options: [ + { + ...options, + groups: [ + 'function', + { newlinesBetween: 'always' }, + 'object', + { newlinesBetween: 'always' }, + 'named', + { newlinesBetween: 'never' }, + 'tuple', + { newlinesBetween: 'ignore' }, + 'nullish', + ], + newlinesBetween: 'always', + }, + ], + output: dedent` + type Type = + (() => void) | + + { a: string } | + + A | + [A] | + + + null + `, + code: dedent` + type Type = + (() => void) | + { a: string } | + + + A | + + [A] | + + + null + `, + }, + ], + valid: [], + }, + ) }) ruleTester.run( diff --git a/test/utils/validate-groups-configuration.test.ts b/test/utils/validate-groups-configuration.test.ts index 404a23ef2..50ffa477d 100644 --- a/test/utils/validate-groups-configuration.test.ts +++ b/test/utils/validate-groups-configuration.test.ts @@ -31,4 +31,19 @@ describe('validate-groups-configuration', () => { ) }).toThrow('Duplicated group(s): predefinedGroup') }) + + it('throws an error with consecutive newlines objects', () => { + expect(() => { + validateGroupsConfiguration( + [ + 'a', + { newlinesBetween: 'always' }, + { newlinesBetween: 'always' }, + 'b', + ], + ['a', 'b'], + [], + ) + }).toThrow("Consecutive 'newlinesBetween' objects are not allowed") + }) }) diff --git a/test/utils/validate-newlines-and-partition-configuration.test.ts b/test/utils/validate-newlines-and-partition-configuration.test.ts index e760d678e..0f89307c9 100644 --- a/test/utils/validate-newlines-and-partition-configuration.test.ts +++ b/test/utils/validate-newlines-and-partition-configuration.test.ts @@ -3,41 +3,49 @@ import { describe, expect, it } from 'vitest' import { validateNewlinesAndPartitionConfiguration } from '../../utils/validate-newlines-and-partition-configuration' describe('validate-newlines-and-partition-configuration', () => { - let partitionByCommentValues: (string[] | boolean | string)[] = [ - true, - 'partitionComment', - ['partition1', 'partition2'], - ] - - it("throws an error when 'partitionComment' is enabled and 'newlinesBetween' is not 'ignore'", () => { + it("throws an error when 'partitionByNewline' is enabled and 'newlinesBetween' is not 'ignore'", () => { let newlinesBetweenValues = ['always', 'never'] as const for (let newlinesBetween of newlinesBetweenValues) { - for (let partitionByComment of partitionByCommentValues) { - expect(() => { - validateNewlinesAndPartitionConfiguration({ - partitionByNewLine: partitionByComment, - newlinesBetween, - }) - }).toThrow( - "The 'partitionByNewLine' and 'newlinesBetween' options cannot be used together", - ) - } + expect(() => { + validateNewlinesAndPartitionConfiguration({ + partitionByNewLine: true, + newlinesBetween, + groups: [], + }) + }).toThrow( + "The 'partitionByNewLine' and 'newlinesBetween' options cannot be used together", + ) } }) - it("allows 'partitionComment' when 'newlinesBetween' is 'ignore'", () => { - for (let partitionByComment of partitionByCommentValues) { + it("throws an error when 'partitionByNewline' is enabled and 'newlinesBetween' objects exist in 'groups'", () => { + let newlinesBetweenValues = ['always', 'never', 'ignore'] as const + + for (let newlinesBetween of newlinesBetweenValues) { expect(() => { validateNewlinesAndPartitionConfiguration({ - partitionByNewLine: partitionByComment, + groups: [{ newlinesBetween }], newlinesBetween: 'ignore', + partitionByNewLine: true, }) - }).not.toThrow() + }).toThrow( + "'newlinesBetween' objects can not be used in 'groups' alongside 'partitionByNewLine'", + ) } }) - it("allows 'newlinesBetween' when 'partitionByComment' is 'false'", () => { + it("allows 'partitionByNewline' when 'newlinesBetween' is 'ignore'", () => { + expect(() => { + validateNewlinesAndPartitionConfiguration({ + newlinesBetween: 'ignore', + partitionByNewLine: true, + groups: [], + }) + }).not.toThrow() + }) + + it("allows 'newlinesBetween' when 'partitionByNewline' is 'false'", () => { let newlinesBetweenValues = ['always', 'never', 'ignore'] as const for (let newlinesBetween of newlinesBetweenValues) { @@ -45,6 +53,7 @@ describe('validate-newlines-and-partition-configuration', () => { validateNewlinesAndPartitionConfiguration({ partitionByNewLine: false, newlinesBetween, + groups: [], }) }).not.toThrow() } diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts index 2c447fe9d..817fb1aaa 100644 --- a/utils/common-json-schemas.ts +++ b/utils/common-json-schemas.ts @@ -53,6 +53,12 @@ export let specialCharactersJsonSchema: JSONSchema4 = { type: 'string', } +export let newlinesBetweenJsonSchema: JSONSchema4 = { + description: 'Specifies how new lines should be handled between groups.', + enum: ['ignore', 'always', 'never'], + type: 'string', +} + export let groupsJsonSchema: JSONSchema4 = { items: { oneOf: [ @@ -65,6 +71,12 @@ export let groupsJsonSchema: JSONSchema4 = { }, type: 'array', }, + { + properties: { + newlinesBetween: newlinesBetweenJsonSchema, + }, + type: 'object', + }, ], }, description: 'Specifies the order of the groups.', @@ -126,12 +138,6 @@ export let partitionByNewLineJsonSchema: JSONSchema4 = { type: 'boolean', } -export let newlinesBetweenJsonSchema: JSONSchema4 = { - description: 'Specifies how new lines should be handled between groups.', - enum: ['ignore', 'always', 'never'], - type: 'string', -} - export let buildUseConfigurationIfJsonSchema = ({ additionalProperties, }: { diff --git a/utils/get-custom-groups-compare-options.ts b/utils/get-custom-groups-compare-options.ts index 1b7244b03..dcdabcc7d 100644 --- a/utils/get-custom-groups-compare-options.ts +++ b/utils/get-custom-groups-compare-options.ts @@ -2,11 +2,15 @@ import type { SortingNode } from '../types/sorting-node' import type { CompareOptions } from './compare' interface Options { + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] customGroups: Record | CustomGroup[] type: 'alphabetical' | 'line-length' | 'natural' | 'custom' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable - groups: (string[] | string)[] order: 'desc' | 'asc' ignoreCase: boolean alphabet: string diff --git a/utils/get-group-number.ts b/utils/get-group-number.ts index d5b2f544d..2ffe6e1e6 100644 --- a/utils/get-group-number.ts +++ b/utils/get-group-number.ts @@ -1,9 +1,11 @@ import type { SortingNode } from '../types/sorting-node' -export let getGroupNumber = ( - groups: (string[] | string)[], - node: SortingNode, -): number => { +type Group = + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + +export let getGroupNumber = (groups: Group[], node: SortingNode): number => { for (let max = groups.length, i = 0; i < max; i++) { let currentGroup = groups[i] diff --git a/utils/get-newlines-between-option.ts b/utils/get-newlines-between-option.ts index 4aefb813d..39fe52258 100644 --- a/utils/get-newlines-between-option.ts +++ b/utils/get-newlines-between-option.ts @@ -9,9 +9,13 @@ export interface GetNewlinesBetweenOptionParameters { } interface Options { + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] customGroups?: Record | CustomGroup[] newlinesBetween: 'ignore' | 'always' | 'never' - groups: (string[] | string)[] } interface CustomGroup { @@ -45,30 +49,38 @@ export let getNewlinesBetweenOption = ({ nodeGroupNumber, }) - if (!options.customGroups || !Array.isArray(options.customGroups)) { - return globalNewlinesBetweenOption - } - let nodeGroup = options.groups[nodeGroupNumber] let nextNodeGroup = options.groups[nextNodeGroupNumber] - if (Array.isArray(nodeGroup) || Array.isArray(nextNodeGroup)) { - return globalNewlinesBetweenOption - } - - let nodeCustomGroup = options.customGroups.find( - customGroup => customGroup.groupName === nodeGroup, - ) - let nextNodeCustomGroup = options.customGroups.find( - customGroup => customGroup.groupName === nextNodeGroup, - ) - + // NewlinesInside check if ( - nodeCustomGroup && - nextNodeCustomGroup && - nodeCustomGroup.groupName === nextNodeCustomGroup.groupName + Array.isArray(options.customGroups) && + typeof nodeGroup === 'string' && + typeof nextNodeGroup === 'string' && + nodeGroup === nextNodeGroup ) { - return nodeCustomGroup.newlinesInside ?? globalNewlinesBetweenOption + let nodeCustomGroup = options.customGroups.find( + customGroup => customGroup.groupName === nodeGroup, + ) + let nextNodeCustomGroup = options.customGroups.find( + customGroup => customGroup.groupName === nextNodeGroup, + ) + + if ( + nodeCustomGroup && + nextNodeCustomGroup && + nodeCustomGroup.groupName === nextNodeCustomGroup.groupName + ) { + return nodeCustomGroup.newlinesInside ?? globalNewlinesBetweenOption + } + } + + // Check if a specific newlinesBetween is defined between the two groups + if (nextNodeGroupNumber === nodeGroupNumber + 2) { + let groupBetween = options.groups[nodeGroupNumber + 1] + if (typeof groupBetween === 'object' && 'newlinesBetween' in groupBetween) { + return groupBetween.newlinesBetween + } } return globalNewlinesBetweenOption diff --git a/utils/get-newlines-errors.ts b/utils/get-newlines-errors.ts index 9ba091912..62504ddde 100644 --- a/utils/get-newlines-errors.ts +++ b/utils/get-newlines-errors.ts @@ -7,9 +7,13 @@ import { getLinesBetween } from './get-lines-between' interface GetNewlinesErrorsParameters { options: { + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] customGroups?: Record | CustomGroup[] newlinesBetween: 'ignore' | 'always' | 'never' - groups: (string[] | string)[] } sourceCode: TSESLint.SourceCode missedSpacingError: T diff --git a/utils/get-options-with-clean-groups.ts b/utils/get-options-with-clean-groups.ts index c2dae4472..372d69601 100644 --- a/utils/get-options-with-clean-groups.ts +++ b/utils/get-options-with-clean-groups.ts @@ -1,5 +1,9 @@ interface GroupOptions { - groups: (string[] | string)[] + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] } export let getOptionsWithCleanGroups = ( @@ -7,9 +11,9 @@ export let getOptionsWithCleanGroups = ( ): T => ({ ...options, groups: options.groups - .filter(group => group.length > 0) + .filter(group => !Array.isArray(group) || group.length > 0) .map(group => - typeof group === 'string' ? group : getCleanedNestedGroups(group), + Array.isArray(group) ? getCleanedNestedGroups(group) : group, ), }) diff --git a/utils/make-newlines-fixes.ts b/utils/make-newlines-fixes.ts index cca9f99b9..17dbbd115 100644 --- a/utils/make-newlines-fixes.ts +++ b/utils/make-newlines-fixes.ts @@ -6,6 +6,16 @@ import { getNewlinesBetweenOption } from './get-newlines-between-option' import { getLinesBetween } from './get-lines-between' import { getNodeRange } from './get-node-range' +interface Options { + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] + customGroups?: Record | CustomGroup[] + newlinesBetween: 'ignore' | 'always' | 'never' +} + interface MakeNewlinesFixesParameters { sourceCode: TSESLint.SourceCode sortedNodes: SortingNode[] @@ -14,12 +24,6 @@ interface MakeNewlinesFixesParameters { options: Options } -interface Options { - customGroups?: Record | CustomGroup[] - newlinesBetween: 'ignore' | 'always' | 'never' - groups: (string[] | string)[] -} - interface CustomGroup { newlinesInside?: 'always' | 'never' groupName: string diff --git a/utils/sort-nodes-by-groups.ts b/utils/sort-nodes-by-groups.ts index e2658ded4..610393ba5 100644 --- a/utils/sort-nodes-by-groups.ts +++ b/utils/sort-nodes-by-groups.ts @@ -15,7 +15,11 @@ interface ExtraOptions { } interface GroupOptions { - groups: (string[] | string)[] + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] } export let sortNodesByGroups = ( diff --git a/utils/use-groups.ts b/utils/use-groups.ts index ae8c1a772..26a72c660 100644 --- a/utils/use-groups.ts +++ b/utils/use-groups.ts @@ -13,7 +13,11 @@ interface UseGroupsValue { } interface UseGroupProps { - groups: (string[] | string)[] + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] } export let useGroups = ({ groups }: UseGroupProps): UseGroupsValue => { diff --git a/utils/validate-generated-groups-configuration.ts b/utils/validate-generated-groups-configuration.ts index 0b05651a5..2858eaeb7 100644 --- a/utils/validate-generated-groups-configuration.ts +++ b/utils/validate-generated-groups-configuration.ts @@ -1,12 +1,17 @@ import { validateNoDuplicatedGroups } from './validate-groups-configuration' -interface Props { +interface ValidateGenerateGroupsConfigurationParameters { customGroups: Record | BaseCustomGroup[] - groups: (string[] | string)[] selectors: string[] modifiers: string[] + groups: Group[] } +type Group = + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + interface BaseCustomGroup { groupName: string } @@ -16,7 +21,7 @@ export let validateGeneratedGroupsConfiguration = ({ selectors, modifiers, groups, -}: Props): void => { +}: ValidateGenerateGroupsConfigurationParameters): void => { let availableCustomGroupNames = new Set( Array.isArray(customGroups) ? customGroups.map(customGroup => customGroup.groupName) @@ -24,6 +29,7 @@ export let validateGeneratedGroupsConfiguration = ({ ) let invalidGroups = groups .flat() + .filter(group => typeof group === 'string') .filter( group => !isPredefinedGroup(selectors, modifiers, group) && diff --git a/utils/validate-groups-configuration.ts b/utils/validate-groups-configuration.ts index 31af72784..b9845bc76 100644 --- a/utils/validate-groups-configuration.ts +++ b/utils/validate-groups-configuration.ts @@ -1,9 +1,14 @@ +type Group = + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + /** * Throws an error if one of the following conditions is met: * - One or more groups specified in `groups` are not predefined nor specified * in `customGroups` * - A group is specified in `groups` more than once - * @param {(string[] | string)[]} groups - The groups to validate. + * @param {Group[]} groups - The groups to validate. * @param {string[]} allowedPredefinedGroups - An array of predefined group * names that are considered valid. * @param {string[]} allowedCustomGroups - An array of custom group names that @@ -11,7 +16,7 @@ * @throws Will throw an error if invalid or duplicated groups are found. */ export let validateGroupsConfiguration = ( - groups: (string[] | string)[], + groups: Group[], allowedPredefinedGroups: string[], allowedCustomGroups: string[], ): void => { @@ -19,9 +24,27 @@ export let validateGroupsConfiguration = ( ...allowedPredefinedGroups, ...allowedCustomGroups, ]) - let invalidGroups = groups - .flat() - .filter(group => !allowedGroupsSet.has(group)) + let invalidGroups: string[] = [] + let isPreviousElementNewlinesBetween = false + for (let groupElement of groups) { + if (typeof groupElement === 'object' && 'newlinesBetween' in groupElement) { + // There should not be two consecutive `newlinesBetween` objects + if (isPreviousElementNewlinesBetween) { + throw new Error("Consecutive 'newlinesBetween' objects are not allowed") + } + isPreviousElementNewlinesBetween = true + } else { + isPreviousElementNewlinesBetween = false + let groupElements = Array.isArray(groupElement) + ? groupElement + : [groupElement] + for (let group of groupElements) { + if (!allowedGroupsSet.has(group)) { + invalidGroups.push(group) + } + } + } + } if (invalidGroups.length) { throw new Error(`Invalid group(s): ${invalidGroups.join(', ')}`) } @@ -30,17 +53,18 @@ export let validateGroupsConfiguration = ( /** * Throws an error if a group is specified more than once - * @param {(string[] | string)[]} groups - The groups to check for duplicates. + * @param {Group[]} groups - The groups to check for duplicates. * @throws Will throw an error if duplicated groups are found. */ -export let validateNoDuplicatedGroups = ( - groups: (string[] | string)[], -): void => { +export let validateNoDuplicatedGroups = (groups: Group[]): void => { let flattenGroups = groups.flat() let seenGroups = new Set() let duplicatedGroups = new Set() for (let group of flattenGroups) { + if (typeof group === 'object' && 'newlinesBetween' in group) { + continue + } if (seenGroups.has(group)) { duplicatedGroups.add(group) } else { diff --git a/utils/validate-newlines-and-partition-configuration.ts b/utils/validate-newlines-and-partition-configuration.ts index 4cc0a1e4e..9bca8cd2f 100644 --- a/utils/validate-newlines-and-partition-configuration.ts +++ b/utils/validate-newlines-and-partition-configuration.ts @@ -1,15 +1,32 @@ interface Options { - partitionByNewLine: string[] | boolean | string + groups: ( + | { newlinesBetween: 'ignore' | 'always' | 'never' } + | string[] + | string + )[] newlinesBetween: 'ignore' | 'always' | 'never' + partitionByNewLine: boolean } export let validateNewlinesAndPartitionConfiguration = ({ partitionByNewLine, newlinesBetween, + groups, }: Options): void => { - if (!!partitionByNewLine && newlinesBetween !== 'ignore') { + if (!partitionByNewLine) { + return + } + if (newlinesBetween !== 'ignore') { throw new Error( "The 'partitionByNewLine' and 'newlinesBetween' options cannot be used together", ) } + let hasNewlinesBetweenGroup = groups.some( + group => typeof group === 'object' && 'newlinesBetween' in group, + ) + if (hasNewlinesBetweenGroup) { + throw new Error( + "'newlinesBetween' objects can not be used in 'groups' alongside 'partitionByNewLine'", + ) + } }