Skip to content

Commit 7c46e82

Browse files
committed
OrderedImports should create separate group for @_implementationOnly imports
1 parent 4706050 commit 7c46e82

File tree

3 files changed

+112
-24
lines changed

3 files changed

+112
-24
lines changed

Documentation/RuleDocumentation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,9 @@ Lint: If a function call with a trailing closure also contains a non-trailing cl
407407
### OrderedImports
408408

409409
Imports must be lexicographically ordered and logically grouped at the top of each source file.
410-
The order of the import groups is 1) regular imports, 2) declaration imports, and 3) @testable
411-
imports. These groups are separated by a single blank line. Blank lines in between the import
412-
declarations are removed.
410+
The order of the import groups is 1) regular imports, 2) declaration imports, 3) @_implementationOnly
411+
imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in
412+
between the import declarations are removed.
413413

414414
Lint: If an import appears anywhere other than the beginning of the file it resides in,
415415
not lexicographically ordered, or not in the appropriate import group, a lint error is

Sources/SwiftFormat/Rules/OrderedImports.swift

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import SwiftSyntax
1414

1515
/// Imports must be lexicographically ordered and logically grouped at the top of each source file.
16-
/// The order of the import groups is 1) regular imports, 2) declaration imports, and 3) @testable
17-
/// imports. These groups are separated by a single blank line. Blank lines in between the import
18-
/// declarations are removed.
16+
/// The order of the import groups is 1) regular imports, 2) declaration imports, 3) @_implementationOnly
17+
/// imports, and 4) @testable imports. These groups are separated by a single blank line. Blank lines in
18+
/// between the import declarations are removed.
1919
///
2020
/// Lint: If an import appears anywhere other than the beginning of the file it resides in,
2121
/// not lexicographically ordered, or not in the appropriate import group, a lint error is
@@ -34,6 +34,7 @@ public final class OrderedImports: SyntaxFormatRule {
3434

3535
var regularImports: [Line] = []
3636
var declImports: [Line] = []
37+
var implementationOnlyImports: [Line] = []
3738
var testableImports: [Line] = []
3839
var codeBlocks: [Line] = []
3940
var fileHeader: [Line] = []
@@ -52,14 +53,16 @@ public final class OrderedImports: SyntaxFormatRule {
5253

5354
regularImports = formatImports(regularImports)
5455
declImports = formatImports(declImports)
56+
implementationOnlyImports = formatImports(implementationOnlyImports)
5557
testableImports = formatImports(testableImports)
5658
formatCodeblocks(&codeBlocks)
5759

58-
let joined = joinLines(fileHeader, regularImports, declImports, testableImports, codeBlocks)
60+
let joined = joinLines(fileHeader, regularImports, declImports, implementationOnlyImports, testableImports, codeBlocks)
5961
formattedLines.append(contentsOf: joined)
6062

6163
regularImports = []
6264
declImports = []
65+
implementationOnlyImports = []
6366
testableImports = []
6467
codeBlocks = []
6568
fileHeader = []
@@ -115,6 +118,11 @@ public final class OrderedImports: SyntaxFormatRule {
115118
regularImports.append(line)
116119
commentBuffer = []
117120

121+
case .implementationOnlyImport:
122+
implementationOnlyImports.append(contentsOf: commentBuffer)
123+
implementationOnlyImports.append(line)
124+
commentBuffer = []
125+
118126
case .testableImport:
119127
testableImports.append(contentsOf: commentBuffer)
120128
testableImports.append(line)
@@ -148,6 +156,7 @@ public final class OrderedImports: SyntaxFormatRule {
148156
/// statements do not appear at the top of the file.
149157
private func checkGrouping<C: Collection>(_ lines: C) where C.Element == Line {
150158
var declGroup = false
159+
var implementationOnlyGroup = false
151160
var testableGroup = false
152161
var codeGroup = false
153162

@@ -157,6 +166,8 @@ public final class OrderedImports: SyntaxFormatRule {
157166
switch lineType {
158167
case .declImport:
159168
declGroup = true
169+
case .implementationOnlyImport:
170+
implementationOnlyGroup = true
160171
case .testableImport:
161172
testableGroup = true
162173
case .codeBlock:
@@ -166,15 +177,15 @@ public final class OrderedImports: SyntaxFormatRule {
166177

167178
if codeGroup {
168179
switch lineType {
169-
case .regularImport, .declImport, .testableImport:
180+
case .regularImport, .declImport, .implementationOnlyImport, .testableImport:
170181
diagnose(.placeAtTopOfFile, on: line.firstToken)
171182
default: ()
172183
}
173184
}
174185

175186
if testableGroup {
176187
switch lineType {
177-
case .regularImport, .declImport:
188+
case .regularImport, .declImport, .implementationOnlyImport:
178189
diagnose(
179190
.groupImports(before: lineType, after: LineType.testableImport),
180191
on: line.firstToken
@@ -183,6 +194,17 @@ public final class OrderedImports: SyntaxFormatRule {
183194
}
184195
}
185196

197+
if implementationOnlyGroup {
198+
switch lineType {
199+
case .regularImport, .declImport:
200+
diagnose(
201+
.groupImports(before: lineType, after: LineType.implementationOnlyImport),
202+
on: line.firstToken
203+
)
204+
default: ()
205+
}
206+
}
207+
186208
if declGroup {
187209
switch lineType {
188210
case .regularImport:
@@ -208,7 +230,7 @@ public final class OrderedImports: SyntaxFormatRule {
208230

209231
for line in imports {
210232
switch line.type {
211-
case .regularImport, .declImport, .testableImport:
233+
case .regularImport, .declImport, .implementationOnlyImport, .testableImport:
212234
let fullyQualifiedImport = line.fullyQualifiedImport
213235
// Check for duplicate imports and potentially remove them.
214236
if let previousMatchingImportIndex = visitedImports[fullyQualifiedImport] {
@@ -390,6 +412,7 @@ fileprivate func convertToCodeBlockItems(lines: [Line]) -> [CodeBlockItemSyntax]
390412
public enum LineType: CustomStringConvertible {
391413
case regularImport
392414
case declImport
415+
case implementationOnlyImport
393416
case testableImport
394417
case codeBlock
395418
case comment
@@ -401,6 +424,8 @@ public enum LineType: CustomStringConvertible {
401424
return "regular"
402425
case .declImport:
403426
return "declaration"
427+
case .implementationOnlyImport:
428+
return "implementationOnly"
404429
case .testableImport:
405430
return "testable"
406431
case .codeBlock:
@@ -515,12 +540,16 @@ fileprivate class Line {
515540

516541
/// Returns a `LineType` the represents the type of import from the given import decl.
517542
private func importType(of importDecl: ImportDeclSyntax) -> LineType {
518-
if let attr = importDecl.attributes.firstToken(viewMode: .sourceAccurate),
519-
attr.tokenKind == .atSign,
520-
attr.nextToken(viewMode: .sourceAccurate)?.text == "testable"
521-
{
543+
544+
let importIdentifierTypes = importDecl.attributes.compactMap { $0.as(AttributeSyntax.self)?.attributeName }
545+
let importAttributeNames = importIdentifierTypes.compactMap { $0.as(IdentifierTypeSyntax.self)?.name.text }
546+
547+
if importAttributeNames.contains("testable") {
522548
return .testableImport
523549
}
550+
if importAttributeNames.contains("_implementationOnly") {
551+
return .implementationOnlyImport
552+
}
524553
if importDecl.importKindSpecifier != nil {
525554
return .declImport
526555
}

Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
2727
import UIKit
2828
2929
@testable import SwiftFormat
30-
8️⃣import enum Darwin.D.isatty
30+
🔟import enum Darwin.D.isatty
3131
// Starts Test
3232
3️⃣@testable import MyModuleUnderTest
3333
// Starts Ind
34-
2️⃣7️⃣import func Darwin.C.isatty
34+
2️⃣8️⃣import func Darwin.C.isatty
35+
36+
// Starts ImplementationOnly
37+
9️⃣@_implementationOnly import InternalModule
3538
3639
let a = 3
37-
4️⃣5️⃣6️⃣import SwiftSyntax
40+
4️⃣5️⃣6️⃣7️⃣import SwiftSyntax
3841
""",
3942
expected: """
4043
// Starts Imports
@@ -48,6 +51,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
4851
import func Darwin.C.isatty
4952
import enum Darwin.D.isatty
5053
54+
// Starts ImplementationOnly
55+
@_implementationOnly import InternalModule
56+
5157
// Starts Test
5258
@testable import MyModuleUnderTest
5359
@testable import SwiftFormat
@@ -60,9 +66,11 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
6066
FindingSpec("3️⃣", message: "sort import statements lexicographically"),
6167
FindingSpec("4️⃣", message: "place imports at the top of the file"),
6268
FindingSpec("5️⃣", message: "place regular imports before testable imports"),
63-
FindingSpec("6️⃣", message: "place regular imports before declaration imports"),
64-
FindingSpec("7️⃣", message: "sort import statements lexicographically"),
65-
FindingSpec("8️⃣", message: "place declaration imports before testable imports"),
69+
FindingSpec("6️⃣", message: "place regular imports before implementationOnly imports"),
70+
FindingSpec("7️⃣", message: "place regular imports before declaration imports"),
71+
FindingSpec("8️⃣", message: "sort import statements lexicographically"),
72+
FindingSpec("9️⃣", message: "place implementationOnly imports before testable imports"),
73+
FindingSpec("🔟", message: "place declaration imports before testable imports"),
6674
]
6775
)
6876
}
@@ -95,6 +103,51 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
95103
]
96104
)
97105
}
106+
107+
func testImportsWithAttributes() {
108+
assertFormatting(
109+
OrderedImports.self,
110+
input: """
111+
import Foundation
112+
1️⃣@preconcurrency import AVFoundation
113+
114+
@preconcurrency @_implementationOnly import InternalModuleC
115+
116+
2️⃣@_implementationOnly import InternalModuleA
117+
118+
3️⃣import Core
119+
120+
@testable @preconcurrency import TestServiceB
121+
4️⃣@preconcurrency @testable import TestServiceA
122+
123+
5️⃣@_implementationOnly @preconcurrency import InternalModuleB
124+
125+
let a = 3
126+
""",
127+
expected: """
128+
@preconcurrency import AVFoundation
129+
import Core
130+
import Foundation
131+
132+
@_implementationOnly import InternalModuleA
133+
@_implementationOnly @preconcurrency import InternalModuleB
134+
@preconcurrency @_implementationOnly import InternalModuleC
135+
136+
@preconcurrency @testable import TestServiceA
137+
@testable @preconcurrency import TestServiceB
138+
139+
let a = 3
140+
""",
141+
findings: [
142+
FindingSpec("1️⃣", message: "sort import statements lexicographically"),
143+
FindingSpec("2️⃣", message: "sort import statements lexicographically"),
144+
FindingSpec("3️⃣", message: "place regular imports before implementationOnly imports"),
145+
FindingSpec("4️⃣", message: "sort import statements lexicographically"),
146+
FindingSpec("5️⃣", message: "place implementationOnly imports before testable imports")
147+
]
148+
)
149+
}
150+
98151

99152
func testImportsOrderWithDocComment() {
100153
assertFormatting(
@@ -145,6 +198,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
145198
import UIKit
146199
147200
import func Darwin.C.isatty
201+
202+
@_implementationOnly import InternalModuleA
203+
@preconcurrency @_implementationOnly import InternalModuleB
148204
149205
@testable import MyModuleUnderTest
150206
""",
@@ -155,6 +211,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
155211
import UIKit
156212
157213
import func Darwin.C.isatty
214+
215+
@_implementationOnly import InternalModuleA
216+
@preconcurrency @_implementationOnly import InternalModuleB
158217
159218
@testable import MyModuleUnderTest
160219
""",
@@ -324,7 +383,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
324383
@testable import testZ // trailing comment about testZ
325384
3️⃣@testable import testC
326385
// swift-format-ignore
327-
@testable import testB
386+
@_implementationOnly import testB
328387
// Comment about Bar
329388
import enum Bar
330389
@@ -350,7 +409,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
350409
@testable import testZ // trailing comment about testZ
351410
352411
// swift-format-ignore
353-
@testable import testB
412+
@_implementationOnly import testB
354413
355414
// Comment about Bar
356415
import enum Bar
@@ -513,7 +572,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
513572
input: """
514573
// exported import of bar
515574
@_exported import bar
516-
@_implementationOnly import bar
575+
@preconcurrency import bar
517576
import bar
518577
import foo
519578
// second import of foo
@@ -531,7 +590,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
531590
expected: """
532591
// exported import of bar
533592
@_exported import bar
534-
@_implementationOnly import bar
593+
@preconcurrency import bar
535594
import bar
536595
// second import of foo
537596
import foo

0 commit comments

Comments
 (0)