diff --git a/CODEOWNERS b/.github/CODEOWNERS similarity index 87% rename from CODEOWNERS rename to .github/CODEOWNERS index 925b7fa14..082c0f746 100644 --- a/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,4 +11,5 @@ * @allevato @hborla @hamishknight @rintaro .github/ @hborla @shahmishal +.github/CODEOWNERS @allevato @hborla @hamishknight @rintaro .swiftci/ @hborla @shahmishal diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 193a9f809..83fb2af1d 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -304,8 +304,9 @@ too long. **description:** Configuration for the `OrderedImports` rule. - `includeConditionalImports` _(boolean)_: Determines whether imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) should be ordered. When `true`, imports inside conditional blocks will be sorted and organized according to the same rules as top-level imports. When `false`, imports within conditional blocks are left in their original order. +- `shouldGroupImports` _(boolean)_: Determines whether different import types should be grouped together. When `true`, imports are grouped into the following order, with a blank line between each section: 1) regular imports, 2) declaration imports, 3) @\_implementationOnly imports, and 4) @testable imports. When `false`, imports are lexicographically ordered by name, regardless of type. -**default:** `{ "includeConditionalImports" : false }` +**default:** `{ "includeConditionalImports" : false, "shouldGroupImports": true }` --- diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index 5bd104103..ed8f2b9cd 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -406,19 +406,22 @@ Lint: If a function call with a trailing closure also contains a non-trailing cl ### OrderedImports -Imports must be lexicographically ordered and logically grouped at the top of each source file. -The order of the import groups is 1) regular imports, 2) declaration imports, 3) @_implementationOnly +Imports must be lexicographically ordered and (optionally) logically grouped at the top of each source file. +The order of the import groups is 1) regular imports, 2) declaration imports, 3) @\_implementationOnly imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in between the import declarations are removed. +Logical grouping is enabled by default but can be disabled via the `orderedImports.shouldGroupImports` +configuration option to limit this rule to lexicographic ordering. + By default, imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) are not ordered. This behavior can be controlled via the `orderedImports.includeConditionalImports` configuration option. Lint: If an import appears anywhere other than the beginning of the file it resides in, - not lexicographically ordered, or not in the appropriate import group, a lint error is + not lexicographically ordered, or (optionally) not in the appropriate import group, a lint error is raised. -Format: Imports will be reordered and grouped at the top of the file. +Format: Imports will be reordered and (optionally) grouped at the top of the file. `OrderedImports` rule can format your code automatically. diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 8fef820c5..18308b06f 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -563,7 +563,8 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable { /// Configuration for the `OrderedImports` rule. public struct OrderedImportsConfiguration: Codable, Equatable { /// Determines whether imports within conditional compilation blocks should be ordered. - public var includeConditionalImports: Bool = false - + public var includeConditionalImports = false + /// Determines whether imports are separated into groups based on their type. + public var shouldGroupImports = true public init() {} } diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index bc82bfa8d..55e3793f7 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -12,19 +12,22 @@ import SwiftSyntax -/// Imports must be lexicographically ordered and logically grouped at the top of each source file. -/// The order of the import groups is 1) regular imports, 2) declaration imports, 3) @_implementationOnly +/// Imports must be lexicographically ordered and (optionally) logically grouped at the top of each source file. +/// The order of the import groups is 1) regular imports, 2) declaration imports, 3) @\_implementationOnly /// imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in /// between the import declarations are removed. /// +/// Logical grouping is enabled by default but can be disabled via the `orderedImports.shouldGroupImports` +/// configuration option to limit this rule to lexicographic ordering. +/// /// By default, imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) are not ordered. /// This behavior can be controlled via the `orderedImports.includeConditionalImports` configuration option. /// /// Lint: If an import appears anywhere other than the beginning of the file it resides in, -/// not lexicographically ordered, or not in the appropriate import group, a lint error is +/// not lexicographically ordered, or (optionally) not in the appropriate import group, a lint error is /// raised. /// -/// Format: Imports will be reordered and grouped at the top of the file. +/// Format: Imports will be reordered and (optionally) grouped at the top of the file. @_spi(Rules) public final class OrderedImports: SyntaxFormatRule { @@ -45,6 +48,7 @@ public final class OrderedImports: SyntaxFormatRule { var declImports: [Line] = [] var implementationOnlyImports: [Line] = [] var testableImports: [Line] = [] + var codeBlocks: [Line] = [] var fileHeader: [Line] = [] var atStartOfFile = true @@ -152,35 +156,51 @@ public final class OrderedImports: SyntaxFormatRule { line.syntaxNode = .ifConfigCodeBlock(CodeBlockItemSyntax(item: .decl(DeclSyntax(ifConfigDecl)))) } - // Separate lines into different categories along with any associated comments. - switch line.type { - case .regularImport: - regularImports.append(contentsOf: commentBuffer) - regularImports.append(line) - commentBuffer = [] + if context.configuration.orderedImports.shouldGroupImports { + // Separate lines into different categories along with any associated comments. + switch line.type { + case .regularImport: + regularImports.append(contentsOf: commentBuffer) + regularImports.append(line) + commentBuffer = [] - case .implementationOnlyImport: - implementationOnlyImports.append(contentsOf: commentBuffer) - implementationOnlyImports.append(line) - commentBuffer = [] + case .implementationOnlyImport: + implementationOnlyImports.append(contentsOf: commentBuffer) + implementationOnlyImports.append(line) + commentBuffer = [] - case .testableImport: - testableImports.append(contentsOf: commentBuffer) - testableImports.append(line) - commentBuffer = [] + case .testableImport: + testableImports.append(contentsOf: commentBuffer) + testableImports.append(line) + commentBuffer = [] - case .declImport: - declImports.append(contentsOf: commentBuffer) - declImports.append(line) - commentBuffer = [] + case .declImport: + declImports.append(contentsOf: commentBuffer) + declImports.append(line) + commentBuffer = [] - case .codeBlock, .blankLine: - codeBlocks.append(contentsOf: commentBuffer) - codeBlocks.append(line) - commentBuffer = [] + case .codeBlock, .blankLine: + codeBlocks.append(contentsOf: commentBuffer) + codeBlocks.append(line) + commentBuffer = [] - case .comment: - commentBuffer.append(line) + case .comment: + commentBuffer.append(line) + } + } else { + switch line.type { + case .regularImport, .implementationOnlyImport, .testableImport, .declImport: + regularImports.append(contentsOf: commentBuffer) + regularImports.append(line) + commentBuffer = [] + case .codeBlock, .blankLine: + codeBlocks.append(contentsOf: commentBuffer) + codeBlocks.append(line) + commentBuffer = [] + + case .comment: + commentBuffer.append(line) + } } } @@ -222,6 +242,10 @@ public final class OrderedImports: SyntaxFormatRule { } } + guard context.configuration.orderedImports.shouldGroupImports else { + continue + } + if testableGroup { switch lineType { case .regularImport, .declImport, .implementationOnlyImport: diff --git a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift index b7a65b33e..932b6a22f 100644 --- a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift @@ -1018,4 +1018,111 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { configuration: configuration ) } + + func testOrderingWithGroupingDisabled() { + var configuration = Configuration.forTesting + configuration.orderedImports.shouldGroupImports = false + + let code = """ + import Core + @testable import func Darwin.C.isatty + import enum Darwin.D.isatty + import Foundation + @_implementationOnly import InternalModule + import MyModuleUnderTest + @testable import SwiftFormat + import SwiftSyntax + import UIKit + + let a = 3 + """ + + assertFormatting( + OrderedImports.self, + input: code, + expected: code, + findings: [], + configuration: configuration + ) + } + + func testMixedContentOrderingWithGroupingDisabled() { + var configuration = Configuration.forTesting + configuration.orderedImports.shouldGroupImports = false + + assertFormatting( + OrderedImports.self, + input: """ + // Header comment + + import Core + let a = 3 + 1️⃣@testable import func Darwin.C.isatty + // Third comment + 2️⃣import Foundation + let b = 4 + // Second comment + 3️⃣4️⃣import enum Darwin.D.isatty + """, + expected: """ + // Header comment + + import Core + @testable import func Darwin.C.isatty + // Second comment + import enum Darwin.D.isatty + // Third comment + import Foundation + + let a = 3 + let b = 4 + """, + findings: [ + FindingSpec("1️⃣", message: "place imports at the top of the file"), + FindingSpec("2️⃣", message: "place imports at the top of the file"), + FindingSpec("3️⃣", message: "place imports at the top of the file"), + FindingSpec("4️⃣", message: "sort import statements lexicographically"), + ], + configuration: configuration + ) + } + + func testInvalidOrderingWithGroupingDisabled() { + var configuration = Configuration.forTesting + configuration.orderedImports.shouldGroupImports = false + + assertFormatting( + OrderedImports.self, + input: """ + import Core + import func Darwin.C.isatty + @testable import SwiftFormat + 1️⃣import enum Darwin.D.isatty + @_implementationOnly import InternalModule + @testable import MyModuleUnderTest + import SwiftSyntax + import Foundation + import UIKit + + let a = 3 + """, + expected: """ + import Core + import func Darwin.C.isatty + import enum Darwin.D.isatty + import Foundation + @_implementationOnly import InternalModule + @testable import MyModuleUnderTest + @testable import SwiftFormat + import SwiftSyntax + import UIKit + + let a = 3 + """, + findings: [ + FindingSpec("1️⃣", message: "sort import statements lexicographically") + ], + configuration: configuration + ) + } }