@@ -12,5 +12,237 @@ import SwiftSyntax
12
12
///
13
13
/// - SeeAlso: https://google.github.io/swift#import-statements
14
14
public final class OrderedImports : SyntaxFormatRule {
15
+ public override func visit( _ node: SourceFileSyntax ) -> Syntax {
16
+ var fileElements = [ CodeBlockItemSyntax] ( )
17
+ fileElements. append ( contentsOf: orderStatements ( node) )
18
+ let statements = SyntaxFactory . makeCodeBlockItemList ( fileElements)
19
+
20
+ return node. withStatements ( statements)
21
+ }
22
+
23
+ /// Gathers all the statements of the sourcefile, separates the imports in their appropriate
24
+ /// group and sorts each group by lexicographically order.
25
+ private func orderStatements( _ node: SourceFileSyntax ) -> [ CodeBlockItemSyntax ] {
26
+ var fileComment = Trivia ( )
27
+ var impComment = Trivia ( )
28
+ var ( allImports, allCode) = getAllImports ( node. statements)
29
+
30
+ if let firstImport = allImports. first,
31
+ let firstStatement = node. statements. first,
32
+ firstImport. statement == firstStatement {
33
+ // Extracts the comments of the imports and separates them into file comments
34
+ // and specific comments for the first import statement.
35
+ ( fileComment, impComment) = getComments ( node. statements. first!)
36
+ allImports [ 0 ] . statement = replaceTrivia (
37
+ on: node. statements. first!,
38
+ token: node. statements. first? . firstToken,
39
+ leadingTrivia: . newlines( 1 ) + impComment
40
+ ) as! CodeBlockItemSyntax
41
+ }
15
42
43
+ let importGroups = groupImports ( allImports)
44
+ let sortedImportGroups = sortImports ( importGroups)
45
+ var allStatements = joinsImports ( sortedImportGroups)
46
+ allStatements. append ( contentsOf: allCode)
47
+
48
+ // After all the imports have been grouped and sorted, the leading trivia of the new first
49
+ // import has to be rewrite to delete any extra newlines and append any file comment.
50
+ if let firstImport = allStatements. first{
51
+ allStatements [ 0 ] = replaceTrivia (
52
+ on: firstImport,
53
+ token: firstImport. firstToken,
54
+ leadingTrivia: fileComment + firstImport. leadingTrivia!. withoutLeadingNewLines ( )
55
+ ) as! CodeBlockItemSyntax
56
+ }
57
+
58
+ return allStatements
59
+ }
60
+
61
+ /// Groups the imports in their appropiate group and returns a collection that contains
62
+ /// the three types of imports groups
63
+ func groupImports( _ imports: ( [ ( statement: CodeBlockItemSyntax , type: ImportType ) ] ) ) ->
64
+ [ [ CodeBlockItemSyntax ] ] {
65
+ var importGroups = [ [ CodeBlockItemSyntax] ( ) , [ CodeBlockItemSyntax] ( ) , [ CodeBlockItemSyntax] ( ) ]
66
+ for imp in imports {
67
+ // Remove all extra blank lines from the import trivia.
68
+ let importWithCleanTrivia = replaceTrivia (
69
+ on: imp. statement,
70
+ token: imp. statement. firstToken,
71
+ leadingTrivia: removeExtraBlankLines ( imp. statement. leadingTrivia!. withoutTrailingSpaces ( ) )
72
+ ) as! CodeBlockItemSyntax
73
+
74
+ importGroups [ imp. type. rawValue] . append ( importWithCleanTrivia)
75
+ }
76
+ return importGroups
77
+ }
78
+
79
+ /// Joins the three types of imports into one single colletion and separates
80
+ /// the import groups with one blank line between them.
81
+ func joinsImports( _ statements: [ [ CodeBlockItemSyntax ] ] ) -> [ CodeBlockItemSyntax ] {
82
+ return statements. flatMap { ( imports: [ CodeBlockItemSyntax ] ) -> [ CodeBlockItemSyntax ] in
83
+ // Ensures only the first import of each group has the
84
+ // blank line separator.
85
+ if statements. first != imports {
86
+ var newImports = imports
87
+ newImports [ 0 ] = replaceTrivia (
88
+ on: imports. first!,
89
+ token: imports. first!. firstToken,
90
+ leadingTrivia: . newlines( 1 ) + imports. first!. leadingTrivia!
91
+ ) as! CodeBlockItemSyntax
92
+ return newImports
93
+ }
94
+ return imports
95
+ }
96
+ }
97
+
98
+ /// Sorts all the import groups by lelexicographically order.
99
+ func sortImports( _ imports: [ [ CodeBlockItemSyntax ] ] ) -> [ [ CodeBlockItemSyntax ] ] {
100
+ return imports. filter { !$0. isEmpty } . map { ( imports: [ CodeBlockItemSyntax ] ) ->
101
+ [ CodeBlockItemSyntax ] in
102
+ if !imports. isEmpty && !isSorted( imports) {
103
+ diagnose ( . sortImports, on: imports. first)
104
+ return imports. sorted ( by: { ( l, r) -> Bool in
105
+ return ( l. item as! ImportDeclSyntax ) . path. description <
106
+ ( r. item as! ImportDeclSyntax ) . path. description
107
+ } )
108
+ }
109
+ else {
110
+ return imports
111
+ }
112
+ }
113
+ }
114
+
115
+ /// Separates all the statements of a file into a collection of the imports
116
+ /// and all the rest of the code.
117
+ func getAllImports( _ statements: CodeBlockItemListSyntax ) ->
118
+ ( [ ( statement: CodeBlockItemSyntax , type: ImportType ) ] ,
119
+ [ CodeBlockItemSyntax ] ) {
120
+ var readerMode : ImportType ?
121
+ var codeEncountered = false
122
+ var allCode = [ CodeBlockItemSyntax] ( )
123
+ // It's assume that most of the staments are not imports.
124
+ allCode. reserveCapacity ( statements. count)
125
+
126
+ let allImports = statements. compactMap { ( statement: CodeBlockItemSyntax ) ->
127
+ ( statement: CodeBlockItemSyntax , type: ImportType ) ? in
128
+ if statement. item is ImportDeclSyntax {
129
+ if codeEncountered {
130
+ diagnose ( . placeAtTheBeginning( importName: statement. description) , on: statement)
131
+ }
132
+
133
+ let newReaderMode = classifyImport ( statement)
134
+ if readerMode != nil && newReaderMode!. rawValue < readerMode!. rawValue {
135
+ diagnose (
136
+ . orderImportsGroups(
137
+ firstImpType: importTypeName ( readerMode!. rawValue) ,
138
+ secondImpType: importTypeName ( newReaderMode!. rawValue)
139
+ ) ,
140
+ on: statement
141
+ )
142
+ }
143
+ readerMode = newReaderMode
144
+ return ( statement, newReaderMode!)
145
+ }
146
+ codeEncountered = true
147
+ allCode. append ( statement)
148
+ return nil
149
+ }
150
+ return ( allImports, allCode)
151
+ }
152
+
153
+ /// If the given trivia contains a blank line between comments, it returns
154
+ /// two trivias, one with all the pieces before the blankline and the other
155
+ /// with the pieces after it
156
+ func getComments( _ firstImp: CodeBlockItemSyntax ) -> ( fileComments: Trivia , impComments: Trivia ) {
157
+ var fileComments = [ TriviaPiece] ( )
158
+ var impComments = [ TriviaPiece] ( )
159
+ guard let firstTokenTrivia = firstImp. leadingTrivia else {
160
+ return ( Trivia ( pieces: fileComments) , Trivia ( pieces: impComments) )
161
+ }
162
+ var hasFoundBlankLine = false
163
+
164
+ for piece in firstTokenTrivia. withoutTrailingSpaces ( ) {
165
+ if !hasFoundBlankLine, case . newlines( let num) = piece, num > 1 {
166
+ hasFoundBlankLine = true
167
+ fileComments. append ( piece)
168
+ }
169
+ else if !hasFoundBlankLine {
170
+ fileComments. append ( piece)
171
+ }
172
+ else {
173
+ impComments. append ( piece)
174
+ }
175
+ }
176
+ return ( Trivia ( pieces: fileComments) , Trivia ( pieces: impComments) )
177
+ }
178
+
179
+ /// Return the given trivia without any set of consecutive blank lines
180
+ func removeExtraBlankLines( _ trivia: Trivia ) -> Trivia {
181
+ var pieces = [ TriviaPiece] ( )
182
+ for piece in trivia. withoutTrailingSpaces ( ) {
183
+ if case . newlines( let num) = piece, num > 1 {
184
+ pieces. append ( . newlines( 1 ) )
185
+ }
186
+ else {
187
+ pieces. append ( piece)
188
+ }
189
+ }
190
+ return Trivia ( pieces: pieces)
191
+ }
192
+
193
+ enum ImportType : Int {
194
+ case regularImport = 0
195
+ case individualImport
196
+ case testableImport
197
+ }
198
+
199
+ /// Indicates to which import type those the given import belongs.
200
+ func classifyImport( _ impStatement: CodeBlockItemSyntax ) -> ImportType ? {
201
+ guard let importToken = impStatement. firstToken else { return nil }
202
+ guard let nextToken = importToken. nextToken else { return nil }
203
+
204
+ if importToken. tokenKind == . atSign && nextToken. text == " testable " {
205
+ return ImportType . testableImport
206
+ }
207
+ if nextToken. tokenKind != . identifier( nextToken. text) {
208
+ return ImportType . individualImport
209
+ }
210
+ return ImportType . regularImport
211
+ }
212
+
213
+ /// Return the name of the given import group type.
214
+ func importTypeName( _ type: Int ) -> String {
215
+ switch type {
216
+ case 0 :
217
+ return " regular imports "
218
+ case 1 :
219
+ return " Individual declaration imports "
220
+ default :
221
+ return " @testable imports "
222
+ }
223
+ }
224
+
225
+ /// Returns a bool indicating if the given collection of imports is ordered by
226
+ /// lexicographically order.
227
+ func isSorted( _ imports: [ CodeBlockItemSyntax ] ) -> Bool {
228
+ for index in 1 ..< imports. count {
229
+ if ( imports [ index - 1 ] . item as! ImportDeclSyntax ) . path. description > ( imports [ index] . item as! ImportDeclSyntax ) . path. description {
230
+ return false
231
+ }
232
+ }
233
+ return true
234
+ }
235
+ }
236
+
237
+ extension Diagnostic . Message {
238
+ static func placeAtTheBeginning( importName: String ) -> Diagnostic . Message {
239
+ return Diagnostic . Message ( . warning, " Place the \( importName) at the beginning of the file. " )
240
+ }
241
+
242
+ static func orderImportsGroups( firstImpType: String , secondImpType: String ) -> Diagnostic . Message {
243
+ return Diagnostic . Message ( . warning, " Place the \( secondImpType) after the \( firstImpType) . " )
244
+ }
245
+
246
+ static let sortImports =
247
+ Diagnostic . Message ( . warning, " Sort the imports by lexicographically order. " )
16
248
}
0 commit comments