Skip to content

Commit 0363aac

Browse files
authored
Order conditionally-compiled imports (#1077)
* Order conditionally-compiled imports * Order conditional imports based on configuration and add nested test case * Preserve comments at start of conditionally compiled block in OrderedImports * Add includeConditionalImports docs to OrderedImports.swift
1 parent bcd6a0f commit 0363aac

File tree

6 files changed

+267
-8
lines changed

6 files changed

+267
-8
lines changed

Documentation/Configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,19 @@ too long.
296296

297297
**default:** `false`
298298

299+
---
300+
301+
### `orderedImports`
302+
**type:** object
303+
304+
**description:** Configuration for the `OrderedImports` rule.
305+
306+
- `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.
307+
308+
**default:** `{ "includeConditionalImports" : false }`
309+
310+
---
311+
299312
> TODO: Add support for enabling/disabling specific syntax transformations in
300313
> the pipeline.
301314

Documentation/RuleDocumentation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ The order of the import groups is 1) regular imports, 2) declaration imports, 3)
411411
imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in
412412
between the import declarations are removed.
413413

414+
By default, imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) are not ordered.
415+
This behavior can be controlled via the `orderedImports.includeConditionalImports` configuration option.
416+
414417
Lint: If an import appears anywhere other than the beginning of the file it resides in,
415418
not lexicographically ordered, or not in the appropriate import group, a lint error is
416419
raised.

Sources/SwiftFormat/API/Configuration+Default.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ extension Configuration {
4343
self.multiElementCollectionTrailingCommas = true
4444
self.reflowMultilineStringLiterals = .never
4545
self.indentBlankLines = false
46+
self.orderedImports = OrderedImportsConfiguration()
4647
}
4748
}

Sources/SwiftFormat/API/Configuration.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public struct Configuration: Codable, Equatable {
4848
case multiElementCollectionTrailingCommas
4949
case reflowMultilineStringLiterals
5050
case indentBlankLines
51+
case orderedImports
5152
}
5253

5354
/// A dictionary containing the default enabled/disabled states of rules, keyed by the rules'
@@ -301,6 +302,9 @@ public struct Configuration: Codable, Equatable {
301302
/// If false (the default), the whitespace in blank lines will be removed entirely.
302303
public var indentBlankLines: Bool
303304

305+
/// Configuration for the `OrderedImports` rule.
306+
public var orderedImports: OrderedImportsConfiguration
307+
304308
/// Creates a new `Configuration` by loading it from a configuration file.
305309
public init(contentsOf url: URL) throws {
306310
let data = try Data(contentsOf: url)
@@ -443,6 +447,13 @@ public struct Configuration: Codable, Equatable {
443447
)
444448
?? defaults.indentBlankLines
445449

450+
self.orderedImports =
451+
try container.decodeIfPresent(
452+
OrderedImportsConfiguration.self,
453+
forKey: .orderedImports
454+
)
455+
?? defaults.orderedImports
456+
446457
// If the `rules` key is not present at all, default it to the built-in set
447458
// so that the behavior is the same as if the configuration had been
448459
// default-initialized. To get an empty rules dictionary, one can explicitly
@@ -481,6 +492,8 @@ public struct Configuration: Codable, Equatable {
481492
try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions)
482493
try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas)
483494
try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals)
495+
try container.encode(indentBlankLines, forKey: .indentBlankLines)
496+
try container.encode(orderedImports, forKey: .orderedImports)
484497
try container.encode(rules, forKey: .rules)
485498
}
486499

@@ -546,3 +559,11 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable {
546559

547560
public init() {}
548561
}
562+
563+
/// Configuration for the `OrderedImports` rule.
564+
public struct OrderedImportsConfiguration: Codable, Equatable {
565+
/// Determines whether imports within conditional compilation blocks should be ordered.
566+
public var includeConditionalImports: Bool = false
567+
568+
public init() {}
569+
}

Sources/SwiftFormat/Rules/OrderedImports.swift

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import SwiftSyntax
1717
/// imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in
1818
/// between the import declarations are removed.
1919
///
20+
/// By default, imports within conditional compilation blocks (`#if`, `#elseif`, `#else`) are not ordered.
21+
/// This behavior can be controlled via the `orderedImports.includeConditionalImports` configuration option.
22+
///
2023
/// Lint: If an import appears anywhere other than the beginning of the file it resides in,
2124
/// not lexicographically ordered, or not in the appropriate import group, a lint error is
2225
/// raised.
@@ -26,7 +29,13 @@ import SwiftSyntax
2629
public final class OrderedImports: SyntaxFormatRule {
2730

2831
public override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax {
29-
let lines = generateLines(codeBlockItemList: node.statements, context: context)
32+
var newNode = node
33+
newNode.statements = orderImports(in: node.statements)
34+
return newNode
35+
}
36+
37+
private func orderImports(in codeBlockItemList: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
38+
let lines = generateLines(codeBlockItemList: codeBlockItemList, context: context)
3039

3140
// Stores the formatted and sorted lines that will be used to reconstruct the list of code block
3241
// items later.
@@ -47,6 +56,10 @@ public final class OrderedImports: SyntaxFormatRule {
4756
// Perform linting on the grouping of the imports.
4857
checkGrouping(linesSection)
4958

59+
if let firstLine = fileHeader.first, firstLine.type == .blankLine {
60+
fileHeader.removeFirst()
61+
}
62+
5063
if let lastLine = fileHeader.last, lastLine.type == .blankLine {
5164
fileHeader.removeLast()
5265
}
@@ -118,6 +131,27 @@ public final class OrderedImports: SyntaxFormatRule {
118131
}
119132
}
120133

134+
if context.configuration.orderedImports.includeConditionalImports,
135+
let syntaxNode = line.syntaxNode,
136+
case .ifConfigCodeBlock(let ifConfigCodeBlock) = syntaxNode
137+
{
138+
var ifConfigDecl = ifConfigCodeBlock.item.cast(IfConfigDeclSyntax.self)
139+
140+
let newClauses = ifConfigDecl.clauses.map { clause in
141+
guard case .statements(let codeBlockItemList) = clause.elements else {
142+
return clause
143+
}
144+
var newClause = clause
145+
var newCodeBlockItemList = orderImports(in: codeBlockItemList)
146+
newCodeBlockItemList.leadingTrivia = .newline + newCodeBlockItemList.leadingTrivia
147+
newClause.elements = .statements(newCodeBlockItemList)
148+
return newClause
149+
}
150+
151+
ifConfigDecl.clauses = IfConfigClauseListSyntax(newClauses)
152+
line.syntaxNode = .ifConfigCodeBlock(CodeBlockItemSyntax(item: .decl(DeclSyntax(ifConfigDecl))))
153+
}
154+
121155
// Separate lines into different categories along with any associated comments.
122156
switch line.type {
123157
case .regularImport:
@@ -154,9 +188,7 @@ public final class OrderedImports: SyntaxFormatRule {
154188
formatAndAppend(linesSection: lines[lastSliceStartIndex..<lines.endIndex])
155189
}
156190

157-
var newNode = node
158-
newNode.statements = CodeBlockItemListSyntax(convertToCodeBlockItems(lines: formattedLines))
159-
return newNode
191+
return CodeBlockItemListSyntax(convertToCodeBlockItems(lines: formattedLines))
160192
}
161193

162194
/// Raise lint errors if the different import types appear in the wrong order, and if import
@@ -354,11 +386,16 @@ private func generateLines(
354386
var blockWithoutTrailingTrivia = block
355387
blockWithoutTrailingTrivia.trailingTrivia = []
356388
currentLine.syntaxNode = .importCodeBlock(blockWithoutTrailingTrivia, sortable: sortable)
389+
} else if block.item.is(IfConfigDeclSyntax.self) {
390+
if currentLine.syntaxNode != nil {
391+
appendNewLine()
392+
}
393+
currentLine.syntaxNode = .ifConfigCodeBlock(block)
357394
} else {
358395
if let syntaxNode = currentLine.syntaxNode {
359396
// Multiple code blocks can be merged, as long as there isn't an import statement.
360397
switch syntaxNode {
361-
case .importCodeBlock:
398+
case .importCodeBlock, .ifConfigCodeBlock:
362399
appendNewLine()
363400
currentLine.syntaxNode = .nonImportCodeBlocks([block])
364401
case .nonImportCodeBlocks(let existingCodeBlocks):
@@ -400,6 +437,8 @@ private func convertToCodeBlockItems(lines: [Line]) -> [CodeBlockItemSyntax] {
400437
switch syntaxNode {
401438
case .importCodeBlock(let codeBlock, _):
402439
append(codeBlockItem: codeBlock)
440+
case .ifConfigCodeBlock(let ifConfigCodeBlock):
441+
append(codeBlockItem: ifConfigCodeBlock)
403442
case .nonImportCodeBlocks(let codeBlocks):
404443
codeBlocks.forEach(append(codeBlockItem:))
405444
}
@@ -458,6 +497,9 @@ private class Line {
458497
case nonImportCodeBlocks([CodeBlockItemSyntax])
459498
/// A single code block item whose content must be an import decl.
460499
case importCodeBlock(CodeBlockItemSyntax, sortable: Bool)
500+
/// A single code block item whose content must be an if config decl.
501+
/// This is used to sort conditional imports.
502+
case ifConfigCodeBlock(CodeBlockItemSyntax)
461503
}
462504

463505
/// Stores line comments. `syntaxNode` need not be defined, since a comment can exist by itself on
@@ -478,7 +520,7 @@ private class Line {
478520
var type: LineType {
479521
if let syntaxNode = syntaxNode {
480522
switch syntaxNode {
481-
case .nonImportCodeBlocks:
523+
case .nonImportCodeBlocks, .ifConfigCodeBlock:
482524
return .codeBlock
483525
case .importCodeBlock(let importCodeBlock, _):
484526
guard let importDecl = importCodeBlock.item.as(ImportDeclSyntax.self) else {
@@ -542,6 +584,8 @@ private class Line {
542584
return codeBlock.firstToken(viewMode: .sourceAccurate)
543585
case .nonImportCodeBlocks(let codeBlocks):
544586
return codeBlocks.first?.firstToken(viewMode: .sourceAccurate)
587+
case .ifConfigCodeBlock(let codeBlock):
588+
return codeBlock.firstToken(viewMode: .sourceAccurate)
545589
}
546590
}
547591

@@ -592,6 +636,8 @@ extension Line: CustomStringConvertible {
592636
description += "\(codeBlocks.count) code blocks "
593637
case .importCodeBlock(_, let sortable):
594638
description += "\(sortable ? "sorted" : "unsorted") import \(importName) "
639+
case .ifConfigCodeBlock:
640+
description += "if config code block "
595641
}
596642
}
597643

0 commit comments

Comments
 (0)