From 64032a76e88ba96e772cd3230c7d79c87a65bb1b Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Sat, 18 Oct 2025 16:39:55 +0300 Subject: [PATCH 1/4] Order conditionally-compiled imports --- .../SwiftFormat/Rules/OrderedImports.swift | 53 ++++++++++++++++--- .../Rules/OrderedImportsTests.swift | 12 +++-- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index 93abebd2..257894a4 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -26,7 +26,16 @@ import SwiftSyntax public final class OrderedImports: SyntaxFormatRule { public override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax { - let lines = generateLines(codeBlockItemList: node.statements, context: context) + var newNode = node + newNode.statements = orderImports(in: node.statements, atStartOfFile: true) + return newNode + } + + private func orderImports( + in codeBlockItemList: CodeBlockItemListSyntax, + atStartOfFile: Bool + ) -> CodeBlockItemListSyntax { + let lines = generateLines(codeBlockItemList: codeBlockItemList, context: context) // Stores the formatted and sorted lines that will be used to reconstruct the list of code block // items later. @@ -38,7 +47,7 @@ public final class OrderedImports: SyntaxFormatRule { var testableImports: [Line] = [] var codeBlocks: [Line] = [] var fileHeader: [Line] = [] - var atStartOfFile = true + var atStartOfFile = atStartOfFile var commentBuffer: [Line] = [] func formatAndAppend(linesSection: ArraySlice) { @@ -118,6 +127,24 @@ public final class OrderedImports: SyntaxFormatRule { } } + if let syntaxNode = line.syntaxNode, case .ifConfigCodeBlock(let ifConfigCodeBlock) = syntaxNode { + var ifConfigDecl = ifConfigCodeBlock.item.cast(IfConfigDeclSyntax.self) + + let newClauses = ifConfigDecl.clauses.map { clause in + guard case .statements(let codeBlockItemList) = clause.elements else { + return clause + } + var newClause = clause + var newCodeBlockItemList = orderImports(in: codeBlockItemList, atStartOfFile: false) + newCodeBlockItemList.leadingTrivia = .newline + newCodeBlockItemList.leadingTrivia + newClause.elements = .statements(newCodeBlockItemList) + return newClause + } + + ifConfigDecl.clauses = IfConfigClauseListSyntax(newClauses) + line.syntaxNode = .ifConfigCodeBlock(CodeBlockItemSyntax(item: .decl(DeclSyntax(ifConfigDecl)))) + } + // Separate lines into different categories along with any associated comments. switch line.type { case .regularImport: @@ -154,9 +181,7 @@ public final class OrderedImports: SyntaxFormatRule { formatAndAppend(linesSection: lines[lastSliceStartIndex.. [CodeBlockItemSyntax] { switch syntaxNode { case .importCodeBlock(let codeBlock, _): append(codeBlockItem: codeBlock) + case .ifConfigCodeBlock(let ifConfigCodeBlock): + append(codeBlockItem: ifConfigCodeBlock) case .nonImportCodeBlocks(let codeBlocks): codeBlocks.forEach(append(codeBlockItem:)) } @@ -458,6 +490,9 @@ private class Line { case nonImportCodeBlocks([CodeBlockItemSyntax]) /// A single code block item whose content must be an import decl. case importCodeBlock(CodeBlockItemSyntax, sortable: Bool) + /// A single code block item whose content must be an if config decl. + /// This is used to sort conditional imports. + case ifConfigCodeBlock(CodeBlockItemSyntax) } /// Stores line comments. `syntaxNode` need not be defined, since a comment can exist by itself on @@ -478,7 +513,7 @@ private class Line { var type: LineType { if let syntaxNode = syntaxNode { switch syntaxNode { - case .nonImportCodeBlocks: + case .nonImportCodeBlocks, .ifConfigCodeBlock: return .codeBlock case .importCodeBlock(let importCodeBlock, _): guard let importDecl = importCodeBlock.item.as(ImportDeclSyntax.self) else { @@ -542,6 +577,8 @@ private class Line { return codeBlock.firstToken(viewMode: .sourceAccurate) case .nonImportCodeBlocks(let codeBlocks): return codeBlocks.first?.firstToken(viewMode: .sourceAccurate) + case .ifConfigCodeBlock(let codeBlock): + return codeBlock.firstToken(viewMode: .sourceAccurate) } } @@ -592,6 +629,8 @@ extension Line: CustomStringConvertible { description += "\(codeBlocks.count) code blocks " case .importCodeBlock(_, let sortable): description += "\(sortable ? "sorted" : "unsorted") import \(importName) " + case .ifConfigCodeBlock: + description += "if config code block " } } diff --git a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift index 9a3c51d6..1d0bb7fd 100644 --- a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift @@ -616,11 +616,13 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { import Zebras 1️⃣import Apples #if canImport(Darwin) - import Darwin + import Foundation + 2️⃣import Darwin #elseif canImport(Glibc) import Glibc + 3️⃣import Foundation #endif - 2️⃣import Aardvarks + 4️⃣import Aardvarks foo() bar() @@ -633,7 +635,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { #if canImport(Darwin) import Darwin + import Foundation #elseif canImport(Glibc) + import Foundation import Glibc #endif @@ -643,7 +647,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { """, findings: [ FindingSpec("1️⃣", message: "sort import statements lexicographically"), - FindingSpec("2️⃣", message: "place imports at the top of the file"), + FindingSpec("2️⃣", message: "sort import statements lexicographically"), + FindingSpec("3️⃣", message: "sort import statements lexicographically"), + FindingSpec("4️⃣", message: "place imports at the top of the file"), ] ) } From db2958c4bd7b6e54798ba72b7d44931238b8bb86 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Tue, 21 Oct 2025 13:22:24 +0300 Subject: [PATCH 2/4] Order conditional imports based on configuration and add nested test case --- Documentation/Configuration.md | 13 +++ Documentation/RuleDocumentation.md | 3 + .../API/Configuration+Default.swift | 1 + Sources/SwiftFormat/API/Configuration.swift | 21 ++++ .../SwiftFormat/Rules/OrderedImports.swift | 5 +- .../Rules/OrderedImportsTests.swift | 104 +++++++++++++++++- 6 files changed, 144 insertions(+), 3 deletions(-) diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 658f8a8d..193a9f80 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -296,6 +296,19 @@ too long. **default:** `false` +--- + +### `orderedImports` +**type:** object + +**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. + +**default:** `{ "includeConditionalImports" : false }` + +--- + > TODO: Add support for enabling/disabling specific syntax transformations in > the pipeline. diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index 510cc070..c2ef7a1b 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -411,6 +411,9 @@ The order of the import groups is 1) regular imports, 2) declaration imports, 3) imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in between the import declarations are removed. +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 raised. diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index ccb4f229..2bf7924e 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -43,5 +43,6 @@ extension Configuration { self.multiElementCollectionTrailingCommas = true self.reflowMultilineStringLiterals = .never self.indentBlankLines = false + self.orderedImports = OrderedImportsConfiguration() } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 9051de17..8fef820c 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -48,6 +48,7 @@ public struct Configuration: Codable, Equatable { case multiElementCollectionTrailingCommas case reflowMultilineStringLiterals case indentBlankLines + case orderedImports } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -301,6 +302,9 @@ public struct Configuration: Codable, Equatable { /// If false (the default), the whitespace in blank lines will be removed entirely. public var indentBlankLines: Bool + /// Configuration for the `OrderedImports` rule. + public var orderedImports: OrderedImportsConfiguration + /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) @@ -443,6 +447,13 @@ public struct Configuration: Codable, Equatable { ) ?? defaults.indentBlankLines + self.orderedImports = + try container.decodeIfPresent( + OrderedImportsConfiguration.self, + forKey: .orderedImports + ) + ?? defaults.orderedImports + // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been // default-initialized. To get an empty rules dictionary, one can explicitly @@ -481,6 +492,8 @@ public struct Configuration: Codable, Equatable { try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions) try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas) try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals) + try container.encode(indentBlankLines, forKey: .indentBlankLines) + try container.encode(orderedImports, forKey: .orderedImports) try container.encode(rules, forKey: .rules) } @@ -546,3 +559,11 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable { public init() {} } + +/// 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 init() {} +} diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index 257894a4..b05f5779 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -127,7 +127,10 @@ public final class OrderedImports: SyntaxFormatRule { } } - if let syntaxNode = line.syntaxNode, case .ifConfigCodeBlock(let ifConfigCodeBlock) = syntaxNode { + if context.configuration.orderedImports.includeConditionalImports, + let syntaxNode = line.syntaxNode, + case .ifConfigCodeBlock(let ifConfigCodeBlock) = syntaxNode + { var ifConfigDecl = ifConfigCodeBlock.item.cast(IfConfigDeclSyntax.self) let newClauses = ifConfigDecl.clauses.map { clause in diff --git a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift index 1d0bb7fd..c3e16eb2 100644 --- a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift @@ -609,7 +609,10 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { ) } - func testConditionalImports() { + func testConditionalImportsWhenEnabled() { + var configuration = Configuration.forTesting + configuration.orderedImports.includeConditionalImports = true + assertFormatting( OrderedImports.self, input: """ @@ -650,7 +653,104 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { FindingSpec("2️⃣", message: "sort import statements lexicographically"), FindingSpec("3️⃣", message: "sort import statements lexicographically"), FindingSpec("4️⃣", message: "place imports at the top of the file"), - ] + ], + configuration: configuration + ) + } + + func testConditionalImportsWhenDisabled() { + var configuration = Configuration.forTesting + configuration.orderedImports.includeConditionalImports = false + + assertFormatting( + OrderedImports.self, + input: """ + import Zebras + 1️⃣import Apples + #if canImport(Darwin) + import Foundation + import Darwin + #elseif canImport(Glibc) + import Glibc + import Foundation + #endif + 2️⃣import Aardvarks + + foo() + bar() + baz() + """, + expected: """ + import Aardvarks + import Apples + import Zebras + + #if canImport(Darwin) + import Foundation + import Darwin + #elseif canImport(Glibc) + import Glibc + import Foundation + #endif + + foo() + bar() + baz() + """, + findings: [ + FindingSpec("1️⃣", message: "sort import statements lexicographically"), + FindingSpec("2️⃣", message: "place imports at the top of the file"), + ], + configuration: configuration + ) + } + + func testNestedConditionalImports() { + var configuration = Configuration() + configuration.orderedImports.includeConditionalImports = true + + assertFormatting( + OrderedImports.self, + input: """ + import A + #if FOO + import D + #if BAR + import F + 1️⃣import E + #else + import H + 2️⃣import G + #endif + 3️⃣5️⃣import C + #endif + 4️⃣import B + """, + expected: """ + import A + import B + + #if FOO + import C + import D + + #if BAR + import E + import F + #else + import G + import H + #endif + #endif + """, + findings: [ + FindingSpec("1️⃣", message: "sort import statements lexicographically"), + FindingSpec("2️⃣", message: "sort import statements lexicographically"), + FindingSpec("3️⃣", message: "place imports at the top of the file"), + FindingSpec("4️⃣", message: "place imports at the top of the file"), + FindingSpec("5️⃣", message: "sort import statements lexicographically"), + ], + configuration: configuration ) } From bd92458f1b4d3d9f91a63e8e9cbe0fde77386119 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Sat, 25 Oct 2025 11:57:06 +0300 Subject: [PATCH 3/4] Preserve comments at start of conditionally compiled block in OrderedImports --- .../SwiftFormat/Rules/OrderedImports.swift | 15 ++-- .../Rules/OrderedImportsTests.swift | 71 ++++++++++++++++++- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index b05f5779..d763a004 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -27,14 +27,11 @@ public final class OrderedImports: SyntaxFormatRule { public override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax { var newNode = node - newNode.statements = orderImports(in: node.statements, atStartOfFile: true) + newNode.statements = orderImports(in: node.statements) return newNode } - private func orderImports( - in codeBlockItemList: CodeBlockItemListSyntax, - atStartOfFile: Bool - ) -> CodeBlockItemListSyntax { + private func orderImports(in codeBlockItemList: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax { let lines = generateLines(codeBlockItemList: codeBlockItemList, context: context) // Stores the formatted and sorted lines that will be used to reconstruct the list of code block @@ -47,7 +44,7 @@ public final class OrderedImports: SyntaxFormatRule { var testableImports: [Line] = [] var codeBlocks: [Line] = [] var fileHeader: [Line] = [] - var atStartOfFile = atStartOfFile + var atStartOfFile = true var commentBuffer: [Line] = [] func formatAndAppend(linesSection: ArraySlice) { @@ -56,6 +53,10 @@ public final class OrderedImports: SyntaxFormatRule { // Perform linting on the grouping of the imports. checkGrouping(linesSection) + if let firstLine = fileHeader.first, firstLine.type == .blankLine { + fileHeader.removeFirst() + } + if let lastLine = fileHeader.last, lastLine.type == .blankLine { fileHeader.removeLast() } @@ -138,7 +139,7 @@ public final class OrderedImports: SyntaxFormatRule { return clause } var newClause = clause - var newCodeBlockItemList = orderImports(in: codeBlockItemList, atStartOfFile: false) + var newCodeBlockItemList = orderImports(in: codeBlockItemList) newCodeBlockItemList.leadingTrivia = .newline + newCodeBlockItemList.leadingTrivia newClause.elements = .statements(newCodeBlockItemList) return newClause diff --git a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift index c3e16eb2..b7a65b33 100644 --- a/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift @@ -706,7 +706,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { } func testNestedConditionalImports() { - var configuration = Configuration() + var configuration = Configuration.forTesting configuration.orderedImports.includeConditionalImports = true assertFormatting( @@ -949,4 +949,73 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase { ] ) } + + func testPreservesEmptyConditionalCompilationBlock() { + var configuration = Configuration.forTesting + configuration.orderedImports.includeConditionalImports = true + + let code = """ + import Apples + import Zebras + + #if FOO + #endif + + foo() + """ + + assertFormatting( + OrderedImports.self, + input: code, + expected: code, + configuration: configuration + ) + } + + func testPreservesHeaderCommentInConditionalCompilationBlock() { + var configuration = Configuration.forTesting + configuration.orderedImports.includeConditionalImports = true + + let code = """ + import Apples + + #if FOO + // Performing FOO-specific logic + + import Foundation + #endif + + foo() + """ + + assertFormatting( + OrderedImports.self, + input: code, + expected: code, + configuration: configuration + ) + } + + func testPreservesCommentsOnlyInConditionalCompilationBlock() { + var configuration = Configuration.forTesting + configuration.orderedImports.includeConditionalImports = true + + let code = """ + import Apples + + #if FOO + // Just a comment + // Another comment + #endif + + foo() + """ + + assertFormatting( + OrderedImports.self, + input: code, + expected: code, + configuration: configuration + ) + } } From 36173f42ab57b745065ffb606d4f2a96bbfb5b2a Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Sat, 25 Oct 2025 13:16:11 +0300 Subject: [PATCH 4/4] Add includeConditionalImports docs to OrderedImports.swift --- Documentation/RuleDocumentation.md | 4 ++-- Sources/SwiftFormat/Rules/OrderedImports.swift | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index c2ef7a1b..3f0977ed 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -411,8 +411,8 @@ The order of the import groups is 1) regular imports, 2) declaration imports, 3) imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in between the import declarations are removed. -By default, imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) are not ordered. This behavior can be controlled via the `orderedImports.includeConditionalImports` -configuration option. +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 diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index d763a004..bc82bfa8 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -17,6 +17,9 @@ import SwiftSyntax /// imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in /// between the import declarations are removed. /// +/// 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 /// raised.