Skip to content

Commit ae522f2

Browse files
authored
OrderedImports should create separate group for @_implementationOnly … (#1013)
* OrderedImports should create separate group for @_implementationOnly imports
1 parent 4706050 commit ae522f2

File tree

4 files changed

+119
-24
lines changed

4 files changed

+119
-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: 47 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,23 @@ 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(
61+
fileHeader,
62+
regularImports,
63+
declImports,
64+
implementationOnlyImports,
65+
testableImports,
66+
codeBlocks
67+
)
5968
formattedLines.append(contentsOf: joined)
6069

6170
regularImports = []
6271
declImports = []
72+
implementationOnlyImports = []
6373
testableImports = []
6474
codeBlocks = []
6575
fileHeader = []
@@ -115,6 +125,11 @@ public final class OrderedImports: SyntaxFormatRule {
115125
regularImports.append(line)
116126
commentBuffer = []
117127

128+
case .implementationOnlyImport:
129+
implementationOnlyImports.append(contentsOf: commentBuffer)
130+
implementationOnlyImports.append(line)
131+
commentBuffer = []
132+
118133
case .testableImport:
119134
testableImports.append(contentsOf: commentBuffer)
120135
testableImports.append(line)
@@ -148,6 +163,7 @@ public final class OrderedImports: SyntaxFormatRule {
148163
/// statements do not appear at the top of the file.
149164
private func checkGrouping<C: Collection>(_ lines: C) where C.Element == Line {
150165
var declGroup = false
166+
var implementationOnlyGroup = false
151167
var testableGroup = false
152168
var codeGroup = false
153169

@@ -157,6 +173,8 @@ public final class OrderedImports: SyntaxFormatRule {
157173
switch lineType {
158174
case .declImport:
159175
declGroup = true
176+
case .implementationOnlyImport:
177+
implementationOnlyGroup = true
160178
case .testableImport:
161179
testableGroup = true
162180
case .codeBlock:
@@ -166,15 +184,15 @@ public final class OrderedImports: SyntaxFormatRule {
166184

167185
if codeGroup {
168186
switch lineType {
169-
case .regularImport, .declImport, .testableImport:
187+
case .regularImport, .declImport, .implementationOnlyImport, .testableImport:
170188
diagnose(.placeAtTopOfFile, on: line.firstToken)
171189
default: ()
172190
}
173191
}
174192

175193
if testableGroup {
176194
switch lineType {
177-
case .regularImport, .declImport:
195+
case .regularImport, .declImport, .implementationOnlyImport:
178196
diagnose(
179197
.groupImports(before: lineType, after: LineType.testableImport),
180198
on: line.firstToken
@@ -183,6 +201,17 @@ public final class OrderedImports: SyntaxFormatRule {
183201
}
184202
}
185203

204+
if implementationOnlyGroup {
205+
switch lineType {
206+
case .regularImport, .declImport:
207+
diagnose(
208+
.groupImports(before: lineType, after: LineType.implementationOnlyImport),
209+
on: line.firstToken
210+
)
211+
default: ()
212+
}
213+
}
214+
186215
if declGroup {
187216
switch lineType {
188217
case .regularImport:
@@ -208,7 +237,7 @@ public final class OrderedImports: SyntaxFormatRule {
208237

209238
for line in imports {
210239
switch line.type {
211-
case .regularImport, .declImport, .testableImport:
240+
case .regularImport, .declImport, .implementationOnlyImport, .testableImport:
212241
let fullyQualifiedImport = line.fullyQualifiedImport
213242
// Check for duplicate imports and potentially remove them.
214243
if let previousMatchingImportIndex = visitedImports[fullyQualifiedImport] {
@@ -390,6 +419,7 @@ fileprivate func convertToCodeBlockItems(lines: [Line]) -> [CodeBlockItemSyntax]
390419
public enum LineType: CustomStringConvertible {
391420
case regularImport
392421
case declImport
422+
case implementationOnlyImport
393423
case testableImport
394424
case codeBlock
395425
case comment
@@ -401,6 +431,8 @@ public enum LineType: CustomStringConvertible {
401431
return "regular"
402432
case .declImport:
403433
return "declaration"
434+
case .implementationOnlyImport:
435+
return "implementationOnly"
404436
case .testableImport:
405437
return "testable"
406438
case .codeBlock:
@@ -515,12 +547,16 @@ fileprivate class Line {
515547

516548
/// Returns a `LineType` the represents the type of import from the given import decl.
517549
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-
{
550+
551+
let importIdentifierTypes = importDecl.attributes.compactMap { $0.as(AttributeSyntax.self)?.attributeName }
552+
let importAttributeNames = importIdentifierTypes.compactMap { $0.as(IdentifierTypeSyntax.self)?.name.text }
553+
554+
if importAttributeNames.contains("testable") {
522555
return .testableImport
523556
}
557+
if importAttributeNames.contains("_implementationOnly") {
558+
return .implementationOnlyImport
559+
}
524560
if importDecl.importKindSpecifier != nil {
525561
return .declImport
526562
}

Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift

Lines changed: 68 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
}
@@ -96,6 +104,50 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
96104
)
97105
}
98106

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+
99151
func testImportsOrderWithDocComment() {
100152
assertFormatting(
101153
OrderedImports.self,
@@ -146,6 +198,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
146198
147199
import func Darwin.C.isatty
148200
201+
@_implementationOnly import InternalModuleA
202+
@preconcurrency @_implementationOnly import InternalModuleB
203+
149204
@testable import MyModuleUnderTest
150205
""",
151206
expected: """
@@ -156,6 +211,9 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
156211
157212
import func Darwin.C.isatty
158213
214+
@_implementationOnly import InternalModuleA
215+
@preconcurrency @_implementationOnly import InternalModuleB
216+
159217
@testable import MyModuleUnderTest
160218
""",
161219
findings: []
@@ -324,7 +382,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
324382
@testable import testZ // trailing comment about testZ
325383
3️⃣@testable import testC
326384
// swift-format-ignore
327-
@testable import testB
385+
@_implementationOnly import testB
328386
// Comment about Bar
329387
import enum Bar
330388
@@ -350,7 +408,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
350408
@testable import testZ // trailing comment about testZ
351409
352410
// swift-format-ignore
353-
@testable import testB
411+
@_implementationOnly import testB
354412
355413
// Comment about Bar
356414
import enum Bar
@@ -513,7 +571,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
513571
input: """
514572
// exported import of bar
515573
@_exported import bar
516-
@_implementationOnly import bar
574+
@preconcurrency import bar
517575
import bar
518576
import foo
519577
// second import of foo
@@ -531,7 +589,7 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
531589
expected: """
532590
// exported import of bar
533591
@_exported import bar
534-
@_implementationOnly import bar
592+
@preconcurrency import bar
535593
import bar
536594
// second import of foo
537595
import foo

api-breakages.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ API breakage: func Configuration.MultilineStringReflowBehavior.hash(into:) has b
1818
API breakage: func Configuration.MultilineStringReflowBehavior.encode(to:) has been removed
1919
API breakage: var Configuration.MultilineStringReflowBehavior.hashValue has been removed
2020
API breakage: constructor Configuration.MultilineStringReflowBehavior.init(from:) has been removed
21+
API breakage: enumelement LineType.implementationOnlyImport has been added as a new enum case

0 commit comments

Comments
 (0)