Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Documentation/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions Documentation/RuleDocumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftFormat/API/Configuration+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ extension Configuration {
self.multiElementCollectionTrailingCommas = true
self.reflowMultilineStringLiterals = .never
self.indentBlankLines = false
self.orderedImports = OrderedImportsConfiguration()
}
}
21 changes: 21 additions & 0 deletions Sources/SwiftFormat/API/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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() {}
}
58 changes: 52 additions & 6 deletions Sources/SwiftFormat/Rules/OrderedImports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,7 +29,13 @@ 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)
return newNode
}

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
// items later.
Expand All @@ -47,6 +56,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()
}
Comment on lines +59 to +61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handles the case in testPreservesHeaderCommentInConditionalCompilationBlock.

Previously, the newline separating the comment and #if FOO was printed as is as part of fileHeader resulting in an extra newline separating the comment and #if FOO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change for regular file headers if for some reason file header comments have a newline before them. I don't think this is a behavior we need to maintain. What do you think?


if let lastLine = fileHeader.last, lastLine.type == .blankLine {
fileHeader.removeLast()
}
Expand Down Expand Up @@ -118,6 +131,27 @@ public final class OrderedImports: SyntaxFormatRule {
}
}

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
guard case .statements(let codeBlockItemList) = clause.elements else {
return clause
}
var newClause = clause
var newCodeBlockItemList = orderImports(in: codeBlockItemList)
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:
Expand Down Expand Up @@ -154,9 +188,7 @@ public final class OrderedImports: SyntaxFormatRule {
formatAndAppend(linesSection: lines[lastSliceStartIndex..<lines.endIndex])
}

var newNode = node
newNode.statements = CodeBlockItemListSyntax(convertToCodeBlockItems(lines: formattedLines))
return newNode
return CodeBlockItemListSyntax(convertToCodeBlockItems(lines: formattedLines))
}

/// Raise lint errors if the different import types appear in the wrong order, and if import
Expand Down Expand Up @@ -354,11 +386,16 @@ private func generateLines(
var blockWithoutTrailingTrivia = block
blockWithoutTrailingTrivia.trailingTrivia = []
currentLine.syntaxNode = .importCodeBlock(blockWithoutTrailingTrivia, sortable: sortable)
} else if block.item.is(IfConfigDeclSyntax.self) {
if currentLine.syntaxNode != nil {
appendNewLine()
}
currentLine.syntaxNode = .ifConfigCodeBlock(block)
} else {
if let syntaxNode = currentLine.syntaxNode {
// Multiple code blocks can be merged, as long as there isn't an import statement.
switch syntaxNode {
case .importCodeBlock:
case .importCodeBlock, .ifConfigCodeBlock:
appendNewLine()
currentLine.syntaxNode = .nonImportCodeBlocks([block])
case .nonImportCodeBlocks(let existingCodeBlocks):
Expand Down Expand Up @@ -400,6 +437,8 @@ private func convertToCodeBlockItems(lines: [Line]) -> [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:))
}
Expand Down Expand Up @@ -458,6 +497,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
Expand All @@ -478,7 +520,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 {
Expand Down Expand Up @@ -542,6 +584,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)
}
}

Expand Down Expand Up @@ -592,6 +636,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 "
}
}

Expand Down
Loading
Loading