diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx
index d066448ac..fc1c7cd97 100644
--- a/docs/content/rules/sort-interfaces.mdx
+++ b/docs/content/rules/sort-interfaces.mdx
@@ -214,6 +214,18 @@ interface User {
Each group of members (separated by empty lines) is treated independently, and the order within each group is preserved.
+### newlinesBetween
+
+default: `'ignore'`
+
+Specifies how new lines should be handled between interface groups.
+
+- `ignore` — Do not report errors related to new lines 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`.
+
### groupKind
default: `'mixed'`
@@ -363,6 +375,7 @@ Determines the matcher used for patterns in the `partitionByComment`, `ignorePat
specialCharacters: 'keep',
ignorePattern: [],
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
optionalityOrder: 'ignore',
matcher: 'minimatch',
groups: [],
@@ -393,6 +406,7 @@ Determines the matcher used for patterns in the `partitionByComment`, `ignorePat
specialCharacters: 'keep',
ignorePattern: [],
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
optionalityOrder: 'ignore',
matcher: 'minimatch',
groups: [],
diff --git a/docs/content/rules/sort-intersection-types.mdx b/docs/content/rules/sort-intersection-types.mdx
index 944a2ae2e..d7a8bb1e9 100644
--- a/docs/content/rules/sort-intersection-types.mdx
+++ b/docs/content/rules/sort-intersection-types.mdx
@@ -145,6 +145,18 @@ type Employee =
Each group of intersection types (separated by empty lines) is treated independently, and the order within each group is preserved.
+### newlinesBetween
+
+default: `'ignore'`
+
+Specifies how new lines should be handled between intersection type groups.
+
+- `ignore` — Do not report errors related to new lines 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`.
+
### groups
@@ -294,8 +306,9 @@ Determines the matcher used for patterns in the `partitionByComment` option.
order: 'asc',
ignoreCase: true,
specialCharacters: 'keep',
+ partitionByComment: false,
partitionByNewLine: false,
- partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
},
@@ -322,8 +335,9 @@ Determines the matcher used for patterns in the `partitionByComment` option.
order: 'asc',
ignoreCase: true,
specialCharacters: 'keep',
- partitionByNewLine: false,
partitionByComment: false,
+ partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
},
diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx
index ff7840096..7b62a13a6 100644
--- a/docs/content/rules/sort-object-types.mdx
+++ b/docs/content/rules/sort-object-types.mdx
@@ -171,6 +171,18 @@ type User = {
Each group of members (separated by empty lines) is treated independently, and the order within each group is preserved.
+### newlinesBetween
+
+default: `'ignore'`
+
+Specifies how new lines should be handled between object type groups.
+
+- `ignore` — Do not report errors related to new lines 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`.
+
### groupKind
default: `'mixed'`
@@ -320,6 +332,7 @@ Determines the matcher used for patterns in the `partitionByComment` and `custom
specialCharacters: 'keep',
partitionByComment: false,
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
customGroups: {},
@@ -349,6 +362,7 @@ Determines the matcher used for patterns in the `partitionByComment` and `custom
specialCharacters: 'keep',
partitionByComment: false,
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
customGroups: {},
diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx
index ca042af38..94c7ac225 100644
--- a/docs/content/rules/sort-objects.mdx
+++ b/docs/content/rules/sort-objects.mdx
@@ -226,6 +226,18 @@ const user = {
Each group of keys (separated by empty lines) is treated independently, and the order within each group is preserved.
+### newlinesBetween
+
+default: `'ignore'`
+
+Specifies how new lines should be handled between object groups.
+
+- `ignore` — Do not report errors related to new lines 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`.
+
### styledComponents
default: `true`
@@ -362,6 +374,7 @@ Determines the matcher used for patterns in the `partitionByComment`, `ignorePat
specialCharacters: 'keep',
partitionByComment: false,
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
styledComponents: true,
ignorePattern: [],
matcher: 'minimatch',
@@ -393,6 +406,7 @@ Determines the matcher used for patterns in the `partitionByComment`, `ignorePat
specialCharacters: 'keep',
partitionByComment: false,
partitionByNewLine: false,
+ newlinesBetween: 'ignore',
styledComponents: true,
ignorePattern: [],
matcher: 'minimatch',
diff --git a/docs/content/rules/sort-union-types.mdx b/docs/content/rules/sort-union-types.mdx
index afa3a6df8..e057dc316 100644
--- a/docs/content/rules/sort-union-types.mdx
+++ b/docs/content/rules/sort-union-types.mdx
@@ -165,6 +165,18 @@ type CarBrand =
Each group of union types (separated by empty lines) is treated independently, and the order within each group is preserved.
+### newlinesBetween
+
+default: `'ignore'`
+
+Specifies how new lines should be handled between union type groups.
+
+- `ignore` — Do not report errors related to new lines 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`.
+
### groups
@@ -314,8 +326,9 @@ Determines the matcher used for patterns in the `partitionByComment` option.
order: 'asc',
ignoreCase: true,
specialCharacters: 'keep',
- partitionByNewLine: false,
partitionByComment: false,
+ partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
},
@@ -342,8 +355,9 @@ Determines the matcher used for patterns in the `partitionByComment` option.
order: 'asc',
ignoreCase: true,
specialCharacters: 'keep',
- partitionByNewLine: false,
partitionByComment: false,
+ partitionByNewLine: false,
+ newlinesBetween: 'ignore',
matcher: 'minimatch',
groups: [],
},
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index 1e91dbe6e..90ec0614b 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -1,5 +1,4 @@
import type { TSESTree } from '@typescript-eslint/types'
-import type { TSESLint } from '@typescript-eslint/utils'
import { builtinModules } from 'node:module'
@@ -9,11 +8,11 @@ import { validateGroupsConfiguration } from '../utils/validate-groups-configurat
import { getOptionsWithCleanGroups } from '../utils/get-options-with-clean-groups'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
-import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
-import { getNodeRange } from '../utils/get-node-range'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { useGroups } from '../utils/use-groups'
@@ -596,28 +595,19 @@ export default createEslintRule, MESSAGE_ID>({
)
}
- let numberOfEmptyLinesBetween = getLinesBetween(
- sourceCode,
- left,
- right,
- )
- if (
- options.newlinesBetween === 'never' &&
- numberOfEmptyLinesBetween > 0
- ) {
- messageIds.push('extraSpacingBetweenImports')
- }
-
- if (options.newlinesBetween === 'always') {
- if (leftNum < rightNum && numberOfEmptyLinesBetween === 0) {
- messageIds.push('missedSpacingBetweenImports')
- } else if (
- numberOfEmptyLinesBetween > 1 ||
- (leftNum === rightNum && numberOfEmptyLinesBetween > 0)
- ) {
- messageIds.push('extraSpacingBetweenImports')
- }
- }
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenImports',
+ extraSpacingError: 'extraSpacingBetweenImports',
+ options,
+ }),
+ ]
for (let messageId of messageIds) {
context.report({
@@ -629,78 +619,16 @@ export default createEslintRule, MESSAGE_ID>({
rightGroup: right.group,
},
node: right.node,
- fix: fixer => {
- let newlinesFixes: TSESLint.RuleFix[] = []
-
- for (let max = sortedNodes.length, i = 0; i < max; i++) {
- let node = sortedNodes.at(i)!
- let nextNode = sortedNodes.at(i + 1)
-
- if (options.newlinesBetween === 'ignore' || !nextNode) {
- continue
- }
-
- let nodeGroupNumber = getGroupNumber(options.groups, node)
- let nextNodeGroupNumber = getGroupNumber(
- options.groups,
- nextNode,
- )
- let currentNodeRange = getNodeRange(
- nodeList.at(i)!.node,
- sourceCode,
- options,
- )
- let nextNodeRange =
- getNodeRange(
- nodeList.at(i + 1)!.node,
- sourceCode,
- options,
- ).at(0)! - 1
-
- let linesBetweenImports = getLinesBetween(
- sourceCode,
- nodeList.at(i)!,
- nodeList.at(i + 1)!,
- )
-
- if (
- (options.newlinesBetween === 'always' &&
- nodeGroupNumber === nextNodeGroupNumber &&
- linesBetweenImports !== 0) ||
- (options.newlinesBetween === 'never' &&
- linesBetweenImports > 0)
- ) {
- newlinesFixes.push(
- fixer.removeRange([
- currentNodeRange.at(1)!,
- nextNodeRange,
- ]),
- )
- }
- if (
- options.newlinesBetween === 'always' &&
- nodeGroupNumber !== nextNodeGroupNumber
- ) {
- if (linesBetweenImports > 1) {
- newlinesFixes.push(
- fixer.replaceTextRange(
- [currentNodeRange.at(1)!, nextNodeRange],
- '\n',
- ),
- )
- } else if (linesBetweenImports === 0) {
- newlinesFixes.push(
- fixer.insertTextAfterRange(currentNodeRange, '\n'),
- )
- }
- }
- }
-
- return [
- ...makeFixes(fixer, nodeList, sortedNodes, sourceCode),
- ...newlinesFixes,
- ]
- },
+ fix: fixer => [
+ ...makeFixes(fixer, nodeList, sortedNodes, sourceCode),
+ ...makeNewlinesFixes(
+ fixer,
+ nodeList,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts
index 23d303a29..325da9103 100644
--- a/rules/sort-interfaces.ts
+++ b/rules/sort-interfaces.ts
@@ -1,15 +1,17 @@
import type { SortingNode } from '../typings'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
import { isMemberOptional } from '../utils/is-member-optional'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
-import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { useGroups } from '../utils/use-groups'
@@ -20,16 +22,19 @@ import { matches } from '../utils/matches'
type MESSAGE_ID =
| 'unexpectedInterfacePropertiesGroupOrder'
+ | 'missedSpacingBetweenInterfaceMembers'
+ | 'extraSpacingBetweenInterfaceMembers'
| 'unexpectedInterfacePropertiesOrder'
type Group = 'multiline' | 'unknown' | T[number] | 'method'
-type Options = [
+export type Options = [
Partial<{
groupKind: 'optional-first' | 'required-first' | 'mixed'
customGroups: { [key: string]: string[] | string }
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
groups: (Group[] | Group)[]
matcher: 'minimatch' | 'regex'
@@ -110,6 +115,12 @@ export default createEslintRule, MESSAGE_ID>({
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
+ newlinesBetween: {
+ description:
+ 'Specifies how new lines should be handled between object types groups.',
+ enum: ['ignore', 'always', 'never'],
+ type: 'string',
+ },
groupKind: {
description: 'Specifies the order of optional and required nodes.',
enum: ['mixed', 'optional-first', 'required-first'],
@@ -158,6 +169,10 @@ export default createEslintRule, MESSAGE_ID>({
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
unexpectedInterfacePropertiesOrder:
'Expected "{{right}}" to come before "{{left}}".',
+ missedSpacingBetweenInterfaceMembers:
+ 'Missed spacing between "{{left}}" and "{{right}}" interfaces.',
+ extraSpacingBetweenInterfaceMembers:
+ 'Extra spacing between "{{left}}" and "{{right}}" interfaces.',
},
},
defaultOptions: [
@@ -187,6 +202,7 @@ export default createEslintRule, MESSAGE_ID>({
matcher: 'minimatch',
ignorePattern: [],
ignoreCase: true,
+ newlinesBetween: 'ignore',
specialCharacters: 'keep',
customGroups: {},
order: 'asc',
@@ -198,6 +214,7 @@ export default createEslintRule, MESSAGE_ID>({
['multiline', 'method', 'unknown'],
Object.keys(options.customGroups),
)
+ validateNewlinesAndPartitionConfiguration(options)
let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment
@@ -315,25 +332,62 @@ export default createEslintRule, MESSAGE_ID>({
}
pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(options.groups, left)
+ let rightNum = getGroupNumber(options.groups, right)
+
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
+
+ let messageIds: MESSAGE_ID[] = []
+
if (indexOfLeft > indexOfRight) {
- let leftNum = getGroupNumber(options.groups, left)
- let rightNum = getGroupNumber(options.groups, right)
+ messageIds.push(
+ leftNum !== rightNum
+ ? 'unexpectedInterfacePropertiesGroupOrder'
+ : 'unexpectedInterfacePropertiesOrder',
+ )
+ }
+
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenInterfaceMembers',
+ extraSpacingError: 'extraSpacingBetweenInterfaceMembers',
+ options,
+ }),
+ ]
+
+ for (let messageId of messageIds) {
context.report({
- messageId:
- leftNum !== rightNum
- ? 'unexpectedInterfacePropertiesGroupOrder'
- : 'unexpectedInterfacePropertiesOrder',
+ messageId,
data: {
- left: toSingleLine(left.name),
+ left: left.name,
leftGroup: left.group,
- right: toSingleLine(right.name),
+ right: right.name,
rightGroup: right.group,
},
node: right.node,
- fix: fixer =>
- makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ fix: fixer => [
+ ...makeFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ...makeNewlinesFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts
index a911dcb70..5d01f61f3 100644
--- a/rules/sort-intersection-types.ts
+++ b/rules/sort-intersection-types.ts
@@ -1,9 +1,12 @@
import type { SortingNode } from '../typings'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
@@ -17,7 +20,9 @@ import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
type MESSAGE_ID =
+ | 'missedSpacingBetweenIntersectionTypes'
| 'unexpectedIntersectionTypesGroupOrder'
+ | 'extraSpacingBetweenIntersectionTypes'
| 'unexpectedIntersectionTypesOrder'
type Group =
@@ -39,6 +44,7 @@ type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
matcher: 'minimatch' | 'regex'
groups: (Group[] | Group)[]
@@ -127,6 +133,12 @@ export default createEslintRule({
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
+ newlinesBetween: {
+ description:
+ 'Specifies how new lines should be handled between object types groups.',
+ enum: ['ignore', 'always', 'never'],
+ type: 'string',
+ },
},
additionalProperties: false,
},
@@ -136,6 +148,10 @@ export default createEslintRule({
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
unexpectedIntersectionTypesOrder:
'Expected "{{right}}" to come before "{{left}}".',
+ missedSpacingBetweenIntersectionTypes:
+ 'Missed spacing between "{{left}}" and "{{right}}" types.',
+ extraSpacingBetweenIntersectionTypes:
+ 'Extra spacing between "{{left}}" and "{{right}}" types.',
},
},
defaultOptions: [
@@ -160,6 +176,7 @@ export default createEslintRule({
specialCharacters: 'keep',
order: 'asc',
matcher: 'minimatch',
+ newlinesBetween: 'ignore',
partitionByComment: false,
partitionByNewLine: false,
groups: [],
@@ -184,6 +201,7 @@ export default createEslintRule({
],
[],
)
+ validateNewlinesAndPartitionConfiguration(options)
let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment
@@ -280,17 +298,41 @@ export default createEslintRule({
for (let nodes of formattedMembers) {
let sortedNodes = sortNodesByGroups(nodes, options)
+
pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(options.groups, left)
+ let rightNum = getGroupNumber(options.groups, right)
+
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
+
+ let messageIds: MESSAGE_ID[] = []
+
if (indexOfLeft > indexOfRight) {
- let leftNum = getGroupNumber(options.groups, left)
- let rightNum = getGroupNumber(options.groups, right)
+ messageIds.push(
+ leftNum !== rightNum
+ ? 'unexpectedIntersectionTypesGroupOrder'
+ : 'unexpectedIntersectionTypesOrder',
+ )
+ }
+
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenIntersectionTypes',
+ extraSpacingError: 'extraSpacingBetweenIntersectionTypes',
+ options,
+ }),
+ ]
+
+ for (let messageId of messageIds) {
context.report({
- messageId:
- leftNum !== rightNum
- ? 'unexpectedIntersectionTypesGroupOrder'
- : 'unexpectedIntersectionTypesOrder',
+ messageId,
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
@@ -298,8 +340,16 @@ export default createEslintRule({
rightGroup: right.group,
},
node: right.node,
- fix: fixer =>
- makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ fix: fixer => [
+ ...makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ ...makeNewlinesFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
index 20eb81f0c..22fd81507 100644
--- a/rules/sort-object-types.ts
+++ b/rules/sort-object-types.ts
@@ -2,10 +2,13 @@ import type { TSESTree } from '@typescript-eslint/types'
import type { SortingNode } from '../typings'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
@@ -19,6 +22,8 @@ import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
type MESSAGE_ID =
+ | 'missedSpacingBetweenObjectTypeMembers'
+ | 'extraSpacingBetweenObjectTypeMembers'
| 'unexpectedObjectTypesGroupOrder'
| 'unexpectedObjectTypesOrder'
@@ -30,6 +35,7 @@ type Options = [
customGroups: { [key in T[number]]: string[] | string }
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
groups: (Group[] | Group)[]
matcher: 'minimatch' | 'regex'
@@ -103,6 +109,12 @@ export default createEslintRule, MESSAGE_ID>({
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
+ newlinesBetween: {
+ description:
+ 'Specifies how new lines should be handled between object types groups.',
+ enum: ['ignore', 'always', 'never'],
+ type: 'string',
+ },
groupKind: {
description: 'Specifies top-level groups.',
type: 'string',
@@ -151,6 +163,10 @@ export default createEslintRule, MESSAGE_ID>({
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
unexpectedObjectTypesOrder:
'Expected "{{right}}" to come before "{{left}}".',
+ missedSpacingBetweenObjectTypeMembers:
+ 'Missed spacing between "{{left}}" and "{{right}}" types.',
+ extraSpacingBetweenObjectTypeMembers:
+ 'Extra spacing between "{{left}}" and "{{right}}" types.',
},
},
defaultOptions: [
@@ -160,6 +176,7 @@ export default createEslintRule, MESSAGE_ID>({
ignoreCase: true,
specialCharacters: 'keep',
matcher: 'minimatch',
+ newlinesBetween: 'ignore',
partitionByComment: false,
partitionByNewLine: false,
groupKind: 'mixed',
@@ -179,6 +196,7 @@ export default createEslintRule, MESSAGE_ID>({
groupKind: 'mixed',
matcher: 'minimatch',
ignoreCase: true,
+ newlinesBetween: 'ignore',
specialCharacters: 'keep',
customGroups: {},
order: 'asc',
@@ -190,6 +208,7 @@ export default createEslintRule, MESSAGE_ID>({
['multiline', 'method', 'unknown'],
Object.keys(options.customGroups),
)
+ validateNewlinesAndPartitionConfiguration(options)
let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment
@@ -313,16 +332,39 @@ export default createEslintRule, MESSAGE_ID>({
}
pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(options.groups, left)
+ let rightNum = getGroupNumber(options.groups, right)
+
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
+
+ let messageIds: MESSAGE_ID[] = []
+
if (indexOfLeft > indexOfRight) {
- let leftNum = getGroupNumber(options.groups, left)
- let rightNum = getGroupNumber(options.groups, right)
+ messageIds.push(
+ leftNum !== rightNum
+ ? 'unexpectedObjectTypesGroupOrder'
+ : 'unexpectedObjectTypesOrder',
+ )
+ }
+
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenObjectTypeMembers',
+ extraSpacingError: 'extraSpacingBetweenObjectTypeMembers',
+ options,
+ }),
+ ]
+
+ for (let messageId of messageIds) {
context.report({
- messageId:
- leftNum !== rightNum
- ? 'unexpectedObjectTypesGroupOrder'
- : 'unexpectedObjectTypesOrder',
+ messageId,
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
@@ -330,8 +372,16 @@ export default createEslintRule, MESSAGE_ID>({
rightGroup: right.group,
},
node: right.node,
- fix: fixer =>
- makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ fix: fixer => [
+ ...makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ ...makeNewlinesFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts
index 5bd9d7352..5ea018cc9 100644
--- a/rules/sort-objects.ts
+++ b/rules/sort-objects.ts
@@ -1,5 +1,4 @@
import type { TSESTree } from '@typescript-eslint/types'
-import type { TSESLint } from '@typescript-eslint/utils'
import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
@@ -7,16 +6,18 @@ import {
getFirstUnorderedNodeDependentOn,
sortNodesByDependencies,
} from '../utils/sort-nodes-by-dependencies'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
import { getNodeParent } from '../utils/get-node-parent'
-import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { useGroups } from '../utils/use-groups'
@@ -26,7 +27,9 @@ import { pairwise } from '../utils/pairwise'
import { matches } from '../utils/matches'
type MESSAGE_ID =
+ | 'missedSpacingBetweenObjectMembers'
| 'unexpectedObjectsDependencyOrder'
+ | 'extraSpacingBetweenObjectMembers'
| 'unexpectedObjectsGroupOrder'
| 'unexpectedObjectsOrder'
@@ -37,6 +40,7 @@ type Options = [
customGroups: { [key: string]: string[] | string }
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
matcher: 'minimatch' | 'regex'
groups: (Group[] | Group)[]
@@ -111,6 +115,12 @@ export default createEslintRule({
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
+ newlinesBetween: {
+ description:
+ 'Specifies how new lines should be handled between object types groups.',
+ enum: ['ignore', 'always', 'never'],
+ type: 'string',
+ },
styledComponents: {
description: 'Controls whether to sort styled components.',
type: 'boolean',
@@ -171,6 +181,10 @@ export default createEslintRule({
unexpectedObjectsOrder: 'Expected "{{right}}" to come before "{{left}}".',
unexpectedObjectsDependencyOrder:
'Expected dependency "{{right}}" to come before "{{nodeDependentOnRight}}".',
+ missedSpacingBetweenObjectMembers:
+ 'Missed spacing between "{{left}}" and "{{right}}" objects.',
+ extraSpacingBetweenObjectMembers:
+ 'Extra spacing between "{{left}}" and "{{right}}" objects.',
},
},
defaultOptions: [
@@ -204,6 +218,7 @@ export default createEslintRule({
ignorePattern: [],
matcher: 'minimatch',
ignoreCase: true,
+ newlinesBetween: 'ignore',
specialCharacters: 'keep',
customGroups: {},
order: 'asc',
@@ -215,6 +230,7 @@ export default createEslintRule({
['multiline', 'method', 'unknown'],
Object.keys(options.customGroups),
)
+ validateNewlinesAndPartitionConfiguration(options)
let shouldIgnore = false
@@ -389,17 +405,6 @@ export default createEslintRule({
let comments = getCommentsBefore(prop, sourceCode)
let lastProp = accumulator.at(-1)?.at(-1)
- if (
- options.partitionByComment &&
- hasPartitionComment(
- options.partitionByComment,
- comments,
- options.matcher,
- )
- ) {
- accumulator.push([])
- }
-
let name: string
let dependencies: string[] = []
@@ -440,9 +445,15 @@ export default createEslintRule({
}
if (
- options.partitionByNewLine &&
- lastProp &&
- getLinesBetween(sourceCode, lastProp, propSortingNode)
+ (options.partitionByNewLine &&
+ lastProp &&
+ getLinesBetween(sourceCode, lastProp, propSortingNode)) ||
+ (options.partitionByComment &&
+ hasPartitionComment(
+ options.partitionByComment,
+ comments,
+ options.matcher,
+ ))
) {
accumulator.push([])
}
@@ -461,36 +472,68 @@ export default createEslintRule({
.flat(),
)
let nodes = formattedMembers.flat()
+
pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(options.groups, left)
+ let rightNum = getGroupNumber(options.groups, right)
+
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
+ let messageIds: MESSAGE_ID[] = []
+ let firstUnorderedNodeDependentOnRight:
+ | SortingNodeWithDependencies
+ | undefined
+
if (indexOfLeft > indexOfRight) {
- let firstUnorderedNodeDependentOnRight =
+ firstUnorderedNodeDependentOnRight =
getFirstUnorderedNodeDependentOn(right, nodes)
- let leftNum = getGroupNumber(options.groups, left)
- let rightNum = getGroupNumber(options.groups, right)
- let messageId: MESSAGE_ID
if (firstUnorderedNodeDependentOnRight) {
- messageId = 'unexpectedObjectsDependencyOrder'
+ messageIds.push('unexpectedObjectsDependencyOrder')
} else {
- messageId =
+ messageIds.push(
leftNum !== rightNum
? 'unexpectedObjectsGroupOrder'
- : 'unexpectedObjectsOrder'
+ : 'unexpectedObjectsOrder',
+ )
}
+ }
+
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenObjectMembers',
+ extraSpacingError: 'extraSpacingBetweenObjectMembers',
+ options,
+ }),
+ ]
+
+ for (let messageId of messageIds) {
context.report({
messageId,
data: {
- left: toSingleLine(left.name),
+ left: left.name,
leftGroup: left.group,
- right: toSingleLine(right.name),
+ right: right.name,
rightGroup: right.group,
nodeDependentOnRight: firstUnorderedNodeDependentOnRight?.name,
},
node: right.node,
- fix: (fixer: TSESLint.RuleFixer) =>
- makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ fix: fixer => [
+ ...makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ ...makeNewlinesFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts
index 4532526e5..c79feaa13 100644
--- a/rules/sort-union-types.ts
+++ b/rules/sort-union-types.ts
@@ -1,9 +1,12 @@
import type { SortingNode } from '../typings'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
+import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
@@ -16,7 +19,11 @@ import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
-type MESSAGE_ID = 'unexpectedUnionTypesGroupOrder' | 'unexpectedUnionTypesOrder'
+type MESSAGE_ID =
+ | 'missedSpacingBetweenUnionTypes'
+ | 'unexpectedUnionTypesGroupOrder'
+ | 'extraSpacingBetweenUnionTypes'
+ | 'unexpectedUnionTypesOrder'
type Group =
| 'intersection'
@@ -37,6 +44,7 @@ type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
matcher: 'minimatch' | 'regex'
groups: (Group[] | Group)[]
@@ -125,6 +133,12 @@ export default createEslintRule({
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
+ newlinesBetween: {
+ description:
+ 'Specifies how new lines should be handled between object types groups.',
+ enum: ['ignore', 'always', 'never'],
+ type: 'string',
+ },
},
additionalProperties: false,
},
@@ -134,6 +148,10 @@ export default createEslintRule({
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
unexpectedUnionTypesOrder:
'Expected "{{right}}" to come before "{{left}}".',
+ missedSpacingBetweenUnionTypes:
+ 'Missed spacing between "{{left}}" and "{{right}}" types.',
+ extraSpacingBetweenUnionTypes:
+ 'Extra spacing between "{{left}}" and "{{right}}" types.',
},
},
defaultOptions: [
@@ -159,6 +177,7 @@ export default createEslintRule({
order: 'asc',
groups: [],
matcher: 'minimatch',
+ newlinesBetween: 'ignore',
partitionByNewLine: false,
partitionByComment: false,
} as const)
@@ -182,6 +201,7 @@ export default createEslintRule({
],
[],
)
+ validateNewlinesAndPartitionConfiguration(options)
let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment
@@ -277,17 +297,41 @@ export default createEslintRule({
for (let nodes of formattedMembers) {
let sortedNodes = sortNodesByGroups(nodes, options)
+
pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(options.groups, left)
+ let rightNum = getGroupNumber(options.groups, right)
+
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
+
+ let messageIds: MESSAGE_ID[] = []
+
if (indexOfLeft > indexOfRight) {
- let leftNum = getGroupNumber(options.groups, left)
- let rightNum = getGroupNumber(options.groups, right)
+ messageIds.push(
+ leftNum !== rightNum
+ ? 'unexpectedUnionTypesGroupOrder'
+ : 'unexpectedUnionTypesOrder',
+ )
+ }
+
+ messageIds = [
+ ...messageIds,
+ ...getNewlinesErrors({
+ left,
+ leftNum,
+ right,
+ rightNum,
+ sourceCode,
+ missedSpacingError: 'missedSpacingBetweenUnionTypes',
+ extraSpacingError: 'extraSpacingBetweenUnionTypes',
+ options,
+ }),
+ ]
+
+ for (let messageId of messageIds) {
context.report({
- messageId:
- leftNum !== rightNum
- ? 'unexpectedUnionTypesGroupOrder'
- : 'unexpectedUnionTypesOrder',
+ messageId,
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
@@ -295,8 +339,16 @@ export default createEslintRule({
rightGroup: right.group,
},
node: right.node,
- fix: fixer =>
- makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ fix: fixer => [
+ ...makeFixes(fixer, nodes, sortedNodes, sourceCode, options),
+ ...makeNewlinesFixes(
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+ ),
+ ],
})
}
})
diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts
index bbbc2c379..6252a028e 100644
--- a/test/sort-imports.test.ts
+++ b/test/sort-imports.test.ts
@@ -1920,6 +1920,146 @@ describe(ruleName, () => {
invalid: [],
},
)
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import { A } from 'a'
+
+
+ import y from '~/y'
+ import z from '~/z'
+
+ import b from '~/b'
+ `,
+ output: dedent`
+ import { A } from 'a'
+ import b from '~/b'
+ import y from '~/y'
+ import z from '~/z'
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenImports',
+ data: {
+ left: 'a',
+ right: '~/y',
+ },
+ },
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '~/z',
+ right: '~/b',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenImports',
+ data: {
+ left: '~/z',
+ right: '~/b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import c from 'c'; import a from '~/a'
+ `,
+ output: dedent`
+ import c from 'c';
+
+ import a from '~/a'
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ },
+ ],
+ errors: [
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: 'c',
+ right: '~/a',
+ },
+ },
+ ],
+ },
+ {
+ code: dedent`
+ import { A } from 'a'
+
+
+ import c from '~/c'
+ import b from '~/b'
+
+ import d from '~/d'
+ `,
+ output: dedent`
+ import { A } from 'a'
+
+ import b from '~/b'
+ import c from '~/c'
+ import d from '~/d'
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenImports',
+ data: {
+ left: 'a',
+ right: '~/c',
+ },
+ },
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '~/c',
+ right: '~/b',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenImports',
+ data: {
+ left: '~/b',
+ right: '~/d',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/sort-interfaces.test.ts b/test/sort-interfaces.test.ts
index 50e8c9dfd..62ec87a2f 100644
--- a/test/sort-interfaces.test.ts
+++ b/test/sort-interfaces.test.ts
@@ -2,6 +2,8 @@ import { RuleTester } from '@typescript-eslint/rule-tester'
import { afterAll, describe, it } from 'vitest'
import { dedent } from 'ts-dedent'
+import type { Options } from '../rules/sort-interfaces'
+
import rule from '../rules/sort-interfaces'
let ruleName = 'sort-interfaces'
@@ -19,7 +21,7 @@ describe(ruleName, () => {
describe(`${ruleName}: sorting by alphabetical order`, () => {
let type = 'alphabetical-order'
- let options = {
+ let options: Options[0] = {
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
@@ -1047,6 +1049,135 @@ describe(ruleName, () => {
],
invalid: [],
})
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface {
+ a: () => null,
+
+
+ y: "y",
+ z: "z",
+
+ b: "b",
+ }
+ `,
+ output: dedent`
+ interface Interface {
+ a: () => null,
+ b: "b",
+ y: "y",
+ z: "z",
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ groups: ['method', 'unknown'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenInterfaceMembers',
+ data: {
+ left: 'a',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'unexpectedInterfacePropertiesOrder',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenInterfaceMembers',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface {
+ a: () => null,
+
+
+ z: "z",
+ y: "y",
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ output: dedent`
+ interface Interface {
+ a: () => null,
+
+ y: "y",
+ z: "z",
+
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ groups: ['method', 'unknown', 'multiline'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenInterfaceMembers',
+ data: {
+ left: 'a',
+ right: 'z',
+ },
+ },
+ {
+ messageId: 'unexpectedInterfacePropertiesOrder',
+ data: {
+ left: 'z',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenInterfaceMembers',
+ data: {
+ left: 'y',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/sort-intersection-types.test.ts b/test/sort-intersection-types.test.ts
index 629630183..be73e321b 100644
--- a/test/sort-intersection-types.test.ts
+++ b/test/sort-intersection-types.test.ts
@@ -691,6 +691,127 @@ describe(ruleName, () => {
invalid: [],
},
)
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type T =
+ (() => null)
+
+
+ & Y
+ & Z
+
+ & B
+ `,
+ output: dedent`
+ type T =
+ (() => null)
+ & B
+ & Y
+ & Z
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ groups: ['function', 'unknown'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenIntersectionTypes',
+ data: {
+ left: '() => null',
+ right: 'Y',
+ },
+ },
+ {
+ messageId: 'unexpectedIntersectionTypesOrder',
+ data: {
+ left: 'Z',
+ right: 'B',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenIntersectionTypes',
+ data: {
+ left: 'Z',
+ right: 'B',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type T =
+ (() => null)
+
+
+ & Z
+ & Y
+ & "A"
+ `,
+ output: dedent`
+ type T =
+ (() => null)
+
+ & Y
+ & Z
+
+ & "A"
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ groups: ['function', 'unknown', 'literal'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenIntersectionTypes',
+ data: {
+ left: '() => null',
+ right: 'Z',
+ },
+ },
+ {
+ messageId: 'unexpectedIntersectionTypesOrder',
+ data: {
+ left: 'Z',
+ right: 'Y',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenIntersectionTypes',
+ data: {
+ left: 'Y',
+ right: '"A"',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts
index dad6f3e45..34e30dc15 100644
--- a/test/sort-object-types.test.ts
+++ b/test/sort-object-types.test.ts
@@ -889,6 +889,135 @@ describe(ruleName, () => {
],
invalid: [],
})
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type Type = {
+ a: () => null,
+
+
+ y: "y",
+ z: "z",
+
+ b: "b",
+ }
+ `,
+ output: dedent`
+ type Type = {
+ a: () => null,
+ b: "b",
+ y: "y",
+ z: "z",
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ groups: ['method', 'unknown'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenObjectTypeMembers',
+ data: {
+ left: 'a',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenObjectTypeMembers',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type Type = {
+ a: () => null,
+
+
+ z: "z",
+ y: "y",
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ output: dedent`
+ type Type = {
+ a: () => null,
+
+ y: "y",
+ z: "z",
+
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ groups: ['method', 'unknown', 'multiline'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenObjectTypeMembers',
+ data: {
+ left: 'a',
+ right: 'z',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ left: 'z',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenObjectTypeMembers',
+ data: {
+ left: 'y',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/sort-objects.test.ts b/test/sort-objects.test.ts
index 65454dbb0..4b5fe8ad8 100644
--- a/test/sort-objects.test.ts
+++ b/test/sort-objects.test.ts
@@ -332,7 +332,7 @@ describe(ruleName, () => {
iHaveFooInMyName: string,
meTooIHaveFoo: string,
a: string,
- b: string,
+ b: "b",
}
`,
options: [
@@ -1646,6 +1646,135 @@ describe(ruleName, () => {
invalid: [],
},
)
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ let Obj = {
+ a: () => null,
+
+
+ y: "y",
+ z: "z",
+
+ b: "b",
+ }
+ `,
+ output: dedent`
+ let Obj = {
+ a: () => null,
+ b: "b",
+ y: "y",
+ z: "z",
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ groups: ['method', 'unknown'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenObjectMembers',
+ data: {
+ left: 'a',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenObjectMembers',
+ data: {
+ left: 'z',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ let Obj = {
+ a: () => null,
+
+
+ z: "z",
+ y: "y",
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ output: dedent`
+ let Obj = {
+ a: () => null,
+
+ y: "y",
+ z: "z",
+
+ b: {
+ // Newline stuff
+ },
+ }
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ groups: ['method', 'unknown', 'multiline'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenObjectMembers',
+ data: {
+ left: 'a',
+ right: 'z',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ left: 'z',
+ right: 'y',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenObjectMembers',
+ data: {
+ left: 'y',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/sort-union-types.test.ts b/test/sort-union-types.test.ts
index 923e917b3..c65b91ff8 100644
--- a/test/sort-union-types.test.ts
+++ b/test/sort-union-types.test.ts
@@ -694,6 +694,127 @@ describe(ruleName, () => {
invalid: [],
},
)
+
+ describe(`${ruleName}: newlinesBetween`, () => {
+ ruleTester.run(
+ `${ruleName}(${type}): removes newlines when never`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type T =
+ (() => null)
+
+
+ | Y
+ | Z
+
+ | B
+ `,
+ output: dedent`
+ type T =
+ (() => null)
+ | B
+ | Y
+ | Z
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'never',
+ groups: ['function', 'unknown'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenUnionTypes',
+ data: {
+ left: '() => null',
+ right: 'Y',
+ },
+ },
+ {
+ messageId: 'unexpectedUnionTypesOrder',
+ data: {
+ left: 'Z',
+ right: 'B',
+ },
+ },
+ {
+ messageId: 'extraSpacingBetweenUnionTypes',
+ data: {
+ left: 'Z',
+ right: 'B',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): keeps one newline when always`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ type T =
+ (() => null)
+
+
+ | Z
+ | Y
+ | "A"
+ `,
+ output: dedent`
+ type T =
+ (() => null)
+
+ | Y
+ | Z
+
+ | "A"
+ `,
+ options: [
+ {
+ ...options,
+ newlinesBetween: 'always',
+ groups: ['function', 'unknown', 'literal'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'extraSpacingBetweenUnionTypes',
+ data: {
+ left: '() => null',
+ right: 'Z',
+ },
+ },
+ {
+ messageId: 'unexpectedUnionTypesOrder',
+ data: {
+ left: 'Z',
+ right: 'Y',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenUnionTypes',
+ data: {
+ left: 'Y',
+ right: '"A"',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/validate-newlines-and-partition-configuration.test.ts b/test/validate-newlines-and-partition-configuration.test.ts
new file mode 100644
index 000000000..c19d40e89
--- /dev/null
+++ b/test/validate-newlines-and-partition-configuration.test.ts
@@ -0,0 +1,52 @@
+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'", () => {
+ let newlinesBetweenValues = ['always', 'never'] as const
+
+ for (let newlinesBetween of newlinesBetweenValues) {
+ for (let partitionByComment of partitionByCommentValues) {
+ expect(() => {
+ validateNewlinesAndPartitionConfiguration({
+ newlinesBetween,
+ partitionByNewLine: partitionByComment,
+ })
+ }).toThrow(
+ "The 'partitionByNewLine' and 'newlinesBetween' options cannot be used together",
+ )
+ }
+ }
+ })
+
+ it("allows 'partitionComment' when 'newlinesBetween' is 'ignore'", () => {
+ for (let partitionByComment of partitionByCommentValues) {
+ expect(() => {
+ validateNewlinesAndPartitionConfiguration({
+ newlinesBetween: 'ignore',
+ partitionByNewLine: partitionByComment,
+ })
+ }).not.toThrow()
+ }
+ })
+
+ it("allows 'newlinesBetween' when 'partitionByComment' is 'false'", () => {
+ let newlinesBetweenValues = ['always', 'never', 'ignore'] as const
+
+ for (let newlinesBetween of newlinesBetweenValues) {
+ expect(() => {
+ validateNewlinesAndPartitionConfiguration({
+ newlinesBetween,
+ partitionByNewLine: false,
+ })
+ }).not.toThrow()
+ }
+ })
+})
diff --git a/utils/get-newlines-errors.ts b/utils/get-newlines-errors.ts
new file mode 100644
index 000000000..fa83bbb78
--- /dev/null
+++ b/utils/get-newlines-errors.ts
@@ -0,0 +1,51 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import type { SortingNode } from '../typings'
+
+import { getLinesBetween } from './get-lines-between'
+
+interface Options {
+ newlinesBetween: 'ignore' | 'always' | 'never'
+}
+
+interface Props {
+ sourceCode: TSESLint.SourceCode
+ missedSpacingError: T
+ extraSpacingError: T
+ right: SortingNode
+ left: SortingNode
+ rightNum: number
+ options: Options
+ leftNum: number
+}
+
+export let getNewlinesErrors = ({
+ missedSpacingError,
+ extraSpacingError,
+ sourceCode,
+ rightNum,
+ leftNum,
+ options,
+ right,
+ left,
+}: Props) => {
+ let errors: T[] = []
+
+ let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
+ if (options.newlinesBetween === 'never' && numberOfEmptyLinesBetween > 0) {
+ errors.push(extraSpacingError)
+ }
+
+ if (options.newlinesBetween === 'always') {
+ if (leftNum < rightNum && numberOfEmptyLinesBetween === 0) {
+ errors.push(missedSpacingError)
+ } else if (
+ numberOfEmptyLinesBetween > 1 ||
+ (leftNum === rightNum && numberOfEmptyLinesBetween > 0)
+ ) {
+ errors.push(extraSpacingError)
+ }
+ }
+
+ return errors
+}
diff --git a/utils/make-newlines-fixes.ts b/utils/make-newlines-fixes.ts
new file mode 100644
index 000000000..8bbf43605
--- /dev/null
+++ b/utils/make-newlines-fixes.ts
@@ -0,0 +1,95 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import type { SortingNode } from '../typings'
+
+import { getLinesBetween } from './get-lines-between'
+import { getGroupNumber } from './get-group-number'
+import { getNodeRange } from './get-node-range'
+
+export const makeNewlinesFixes = (
+ fixer: TSESLint.RuleFixer,
+ nodes: SortingNode[],
+ sortedNodes: SortingNode[],
+ source: TSESLint.SourceCode,
+ options: {
+ newlinesBetween: 'ignore' | 'always' | 'never'
+ groups: (string[] | string)[]
+ },
+) => {
+ let fixes: TSESLint.RuleFix[] = []
+
+ for (let max = sortedNodes.length, i = 0; i < max; i++) {
+ let sortingNode = sortedNodes.at(i)!
+ let nextSortingNode = sortedNodes.at(i + 1)
+
+ if (options.newlinesBetween === 'ignore' || !nextSortingNode) {
+ continue
+ }
+
+ let nodeGroupNumber = getGroupNumber(options.groups, sortingNode)
+ let nextNodeGroupNumber = getGroupNumber(options.groups, nextSortingNode)
+ let currentNodeRange = getNodeRange(nodes.at(i)!.node, source)
+ let nextNodeRangeStart = getNodeRange(nodes.at(i + 1)!.node, source).at(0)!
+ let rangeToReplace: [number, number] = [
+ currentNodeRange.at(1)!,
+ nextNodeRangeStart,
+ ]
+ let textBetweenNodes = source.text.slice(
+ currentNodeRange.at(1),
+ nextNodeRangeStart,
+ )
+
+ let linesBetweenMembers = getLinesBetween(
+ source,
+ nodes.at(i)!,
+ nodes.at(i + 1)!,
+ )
+
+ let rangeReplacement: undefined | string
+ if (
+ (options.newlinesBetween === 'always' &&
+ nodeGroupNumber === nextNodeGroupNumber &&
+ linesBetweenMembers !== 0) ||
+ (options.newlinesBetween === 'never' && linesBetweenMembers > 0)
+ ) {
+ rangeReplacement = getStringWithoutInvalidNewlines(textBetweenNodes)
+ }
+
+ if (
+ options.newlinesBetween === 'always' &&
+ nodeGroupNumber !== nextNodeGroupNumber &&
+ linesBetweenMembers !== 1
+ ) {
+ rangeReplacement = addNewlineBeforeFirstNewline(
+ linesBetweenMembers > 1
+ ? getStringWithoutInvalidNewlines(textBetweenNodes)
+ : textBetweenNodes,
+ )
+ let isOnSameLine =
+ linesBetweenMembers === 0 &&
+ nodes.at(i)!.node.loc.end.line === nodes.at(i + 1)!.node.loc.start.line
+ if (isOnSameLine) {
+ rangeReplacement = addNewlineBeforeFirstNewline(rangeReplacement)
+ }
+ }
+
+ if (rangeReplacement) {
+ fixes.push(fixer.replaceTextRange(rangeToReplace, rangeReplacement))
+ }
+ }
+
+ return fixes
+}
+
+const getStringWithoutInvalidNewlines = (value: string) =>
+ value.replaceAll(/\n+\s*\n+/g, '\n').replaceAll(/\n+/g, '\n')
+
+const addNewlineBeforeFirstNewline = (value: string) => {
+ let firstNewlineIndex = value.indexOf('\n')
+ if (firstNewlineIndex === -1) {
+ return value + '\n'
+ }
+ return (
+ value.slice(0, firstNewlineIndex) + '\n' + value.slice(firstNewlineIndex)
+ )
+}
diff --git a/utils/sort-nodes-by-dependencies.ts b/utils/sort-nodes-by-dependencies.ts
index e26d735c5..a6a9adab0 100644
--- a/utils/sort-nodes-by-dependencies.ts
+++ b/utils/sort-nodes-by-dependencies.ts
@@ -53,10 +53,12 @@ export let sortNodesByDependencies = (
* Returns the first node that is dependent on the given node, but is not
* ordered before it
*/
-export let getFirstUnorderedNodeDependentOn = (
- node: SortingNodeWithDependencies,
- currentlyOrderedNodes: SortingNodeWithDependencies[],
-): SortingNodeWithDependencies | undefined => {
+export let getFirstUnorderedNodeDependentOn = <
+ T extends SortingNodeWithDependencies,
+>(
+ node: T,
+ currentlyOrderedNodes: T[],
+): undefined | T => {
let nodesDependentOnNode = currentlyOrderedNodes.filter(
currentlyOrderedNode =>
currentlyOrderedNode.dependencies.includes(
diff --git a/utils/validate-newlines-and-partition-configuration.ts b/utils/validate-newlines-and-partition-configuration.ts
new file mode 100644
index 000000000..b25612286
--- /dev/null
+++ b/utils/validate-newlines-and-partition-configuration.ts
@@ -0,0 +1,15 @@
+interface Options {
+ partitionByNewLine: string[] | boolean | string
+ newlinesBetween: 'ignore' | 'always' | 'never'
+}
+
+export const validateNewlinesAndPartitionConfiguration = ({
+ partitionByNewLine,
+ newlinesBetween,
+}: Options): void => {
+ if (!!partitionByNewLine && newlinesBetween !== 'ignore') {
+ throw new Error(
+ "The 'partitionByNewLine' and 'newlinesBetween' options cannot be used together",
+ )
+ }
+}