@@ -34,12 +34,15 @@ fileprivate extension SymbolOccurrence {
34
34
}
35
35
36
36
extension SourceKitLSPServer {
37
- func workspaceTests( _ req: WorkspaceTestsRequest ) async throws -> [ TestItem ] {
38
- // Gather all tests classes and test methods.
39
- let testSymbolOccurrences = workspaces. flatMap { ( workspace) -> [ SymbolOccurrence ] in
40
- return workspace. index? . unitTests ( ) ?? [ ]
41
- }
42
-
37
+ /// Converts a flat list of test symbol occurrences to hierarchical `TestItem` array, inferring the hierarchical
38
+ /// structure from `childOf` relations between the symbol occurrences.
39
+ ///
40
+ /// If `testLocations` is passed, it's an array of test IDs to the locations of the test cases. This allows us to
41
+ /// provide ranges for the test cases in source code instead of only the test's location that we get from the index.
42
+ private func testItems(
43
+ for testSymbolOccurrences: [ SymbolOccurrence ] ,
44
+ testLocations: [ String : Location ]
45
+ ) -> [ TestItem ] {
43
46
// Arrange tests by the USR they are contained in. This allows us to emit test methods as children of test classes.
44
47
// `occurrencesByParent[nil]` are the root test symbols that aren't a child of another test symbol.
45
48
var occurrencesByParent : [ String ? : [ SymbolOccurrence ] ] = [ : ]
@@ -73,18 +76,24 @@ extension SourceKitLSPServer {
73
76
utf16index: testSymbolOccurrence. location. utf8Column - 1
74
77
)
75
78
76
- let symbolLocation = Location (
77
- uri: DocumentURI ( URL ( fileURLWithPath: testSymbolOccurrence. location. path) ) ,
78
- range: Range ( symbolPosition)
79
- )
79
+ let id = ( context + [ testSymbolOccurrence. symbol. name] ) . joined ( separator: " / " )
80
+ let location : Location
81
+ if let syntacticLocation = testLocations [ id] {
82
+ location = syntacticLocation
83
+ } else {
84
+ location = Location (
85
+ uri: DocumentURI ( URL ( fileURLWithPath: testSymbolOccurrence. location. path) ) ,
86
+ range: Range ( symbolPosition)
87
+ )
88
+ }
80
89
let children =
81
90
occurrencesByParent [ testSymbolOccurrence. symbol. usr, default: [ ] ]
82
91
. sorted ( )
83
92
. map { testItem ( for: $0, context: context + [ testSymbolOccurrence. symbol. name] ) }
84
93
return TestItem (
85
- id: ( context + [ testSymbolOccurrence . symbol . name ] ) . joined ( separator : " / " ) ,
94
+ id: id ,
86
95
label: testSymbolOccurrence. symbol. name,
87
- location: symbolLocation ,
96
+ location: location ,
88
97
children: children,
89
98
tags: [ ]
90
99
)
@@ -95,32 +104,54 @@ extension SourceKitLSPServer {
95
104
. map { testItem ( for: $0, context: [ ] ) }
96
105
}
97
106
107
+ func workspaceTests( _ req: WorkspaceTestsRequest ) async throws -> [ TestItem ] {
108
+ // Gather all tests classes and test methods.
109
+ let testSymbolOccurrences = workspaces. flatMap { ( workspace) -> [ SymbolOccurrence ] in
110
+ return workspace. index? . unitTests ( ) ?? [ ]
111
+ }
112
+ return testItems ( for: testSymbolOccurrences, testLocations: [ : ] )
113
+ }
114
+
115
+ /// Extracts a flat dictionary mapping test IDs to their locations from the given `testItems`.
116
+ private func testLocations( from testItems: [ TestItem ] ) -> [ String : Location ] {
117
+ var result : [ String : Location ] = [ : ]
118
+ for testItem in testItems {
119
+ result [ testItem. id] = testItem. location
120
+ result. merge ( testLocations ( from: testItem. children) ) { old, new in new }
121
+ }
122
+ return result
123
+ }
124
+
98
125
func documentTests(
99
126
_ req: DocumentTestsRequest ,
100
127
workspace: Workspace ,
101
128
languageService: LanguageService
102
- ) async throws -> [ WorkspaceSymbolItem ] ? {
129
+ ) async throws -> [ TestItem ] {
103
130
let snapshot = try self . documentManager. latestSnapshot ( req. textDocument. uri)
104
131
let mainFileUri = await workspace. buildSystemManager. mainFile (
105
132
for: req. textDocument. uri,
106
133
language: snapshot. language
107
134
)
135
+
136
+ let syntacticTests = try await languageService. syntacticDocumentTests ( for: req. textDocument. uri)
137
+ let testLocations = testLocations ( from: syntacticTests)
138
+
108
139
if let index = workspace. index {
109
140
var outOfDateChecker = IndexOutOfDateChecker ( )
110
141
let testSymbols =
111
142
index. unitTests ( referencedByMainFiles: [ mainFileUri. pseudoPath] )
112
143
. filter { $0. canBeTestDefinition && outOfDateChecker. isUpToDate ( $0. location) }
113
144
114
145
if !testSymbols. isEmpty {
115
- return testSymbols . sorted ( ) . map ( WorkspaceSymbolItem . init )
146
+ return testItems ( for : testSymbols , testLocations : testLocations )
116
147
}
117
148
if outOfDateChecker. indexHasUpToDateUnit ( for: mainFileUri. pseudoPath, index: index) {
118
149
// The index is up-to-date and doesn't contain any tests. We don't need to do a syntactic fallback.
119
150
return [ ]
120
151
}
121
152
}
122
153
// We don't have any up-to-date index entries for this file. Syntactically look for tests.
123
- return try await languageService . syntacticDocumentTests ( for : req . textDocument . uri )
154
+ return syntacticTests
124
155
}
125
156
}
126
157
@@ -133,7 +164,7 @@ private final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
133
164
private var snapshot : DocumentSnapshot
134
165
135
166
/// The workspace symbols representing the found `XCTestCase` subclasses and test methods.
136
- private var result : [ WorkspaceSymbolItem ] = [ ]
167
+ private var result : [ TestItem ] = [ ]
137
168
138
169
/// Names of classes that are known to not inherit from `XCTestCase` and can thus be ruled out to be test classes.
139
170
private static let knownNonXCTestSubclasses = [ " NSObject " ]
@@ -146,15 +177,15 @@ private final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
146
177
public static func findTestSymbols(
147
178
in snapshot: DocumentSnapshot ,
148
179
syntaxTreeManager: SyntaxTreeManager
149
- ) async -> [ WorkspaceSymbolItem ] {
180
+ ) async -> [ TestItem ] {
150
181
let syntaxTree = await syntaxTreeManager. syntaxTree ( for: snapshot)
151
182
let visitor = SyntacticSwiftXCTestScanner ( snapshot: snapshot)
152
183
visitor. walk ( syntaxTree)
153
184
return visitor. result
154
185
}
155
186
156
- private func findTestMethods( in members: MemberBlockItemListSyntax , containerName: String ) -> [ WorkspaceSymbolItem ] {
157
- return members. compactMap { ( member) -> WorkspaceSymbolItem ? in
187
+ private func findTestMethods( in members: MemberBlockItemListSyntax , containerName: String ) -> [ TestItem ] {
188
+ return members. compactMap { ( member) -> TestItem ? in
158
189
guard let function = member. decl. as ( FunctionDeclSyntax . self) else {
159
190
return nil
160
191
}
@@ -166,25 +197,29 @@ private final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
166
197
// Test methods can't be static.
167
198
return nil
168
199
}
169
- guard function. signature. returnClause == nil else {
170
- // Test methods can't have a return type.
200
+ guard function. signature. returnClause == nil , function . signature . parameterClause . parameters . isEmpty else {
201
+ // Test methods can't have a return type or have parameters .
171
202
// Technically we are also filtering out functions that have an explicit `Void` return type here but such
172
203
// declarations are probably less common than helper functions that start with `test` and have a return type.
173
204
return nil
174
205
}
175
- guard let position = snapshot. position ( of: function. name. positionAfterSkippingLeadingTrivia) else {
206
+ guard
207
+ let range = snapshot. range (
208
+ of: function. positionAfterSkippingLeadingTrivia..< function. endPositionBeforeTrailingTrivia
209
+ )
210
+ else {
176
211
logger. fault (
177
- " Failed to convert offset \( function. name . positionAfterSkippingLeadingTrivia. utf8Offset) to UTF-16-based position "
212
+ " Failed to convert range \( function. positionAfterSkippingLeadingTrivia. utf8Offset) ..< \( function . endPositionBeforeTrailingTrivia . utf8Offset ) to UTF-16-based line-column range "
178
213
)
179
214
return nil
180
215
}
181
- let symbolInformation = SymbolInformation (
182
- name: function. name. text,
183
- kind: . method,
184
- location: Location ( uri: snapshot. uri, range: Range ( position) ) ,
185
- containerName: containerName
216
+ return TestItem (
217
+ id: " \( containerName) / \( function. name. text) () " ,
218
+ label: " \( function. name. text) () " ,
219
+ location: Location ( uri: snapshot. uri, range: range) ,
220
+ children: [ ] ,
221
+ tags: [ ]
186
222
)
187
- return WorkspaceSymbolItem . symbolInformation ( symbolInformation)
188
223
}
189
224
}
190
225
@@ -207,20 +242,21 @@ private final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
207
242
// Don't report a test class if it doesn't contain any test methods.
208
243
return . visitChildren
209
244
}
210
- guard let position = snapshot. position ( of: node. name. positionAfterSkippingLeadingTrivia) else {
245
+ guard let range = snapshot. range ( of: node. positionAfterSkippingLeadingTrivia..< node. endPositionBeforeTrailingTrivia)
246
+ else {
211
247
logger. fault (
212
- " Failed to convert offset \( node. name . positionAfterSkippingLeadingTrivia. utf8Offset) to UTF-16-based position "
248
+ " Failed to convert range \( node. positionAfterSkippingLeadingTrivia. utf8Offset) ..< \( node . endPositionBeforeTrailingTrivia . utf8Offset ) to UTF-16-based line-column range "
213
249
)
214
250
return . visitChildren
215
251
}
216
- let testClassSymbolInformation = SymbolInformation (
217
- name: node. name. text,
218
- kind: . class,
219
- location: Location ( uri: snapshot. uri, range: Range ( position) ) ,
220
- containerName: nil
252
+ let testItem = TestItem (
253
+ id: node. name. text,
254
+ label: node. name. text,
255
+ location: Location ( uri: snapshot. uri, range: range) ,
256
+ children: testMethods,
257
+ tags: [ ]
221
258
)
222
- result. append ( . symbolInformation( testClassSymbolInformation) )
223
- result += testMethods
259
+ result. append ( testItem)
224
260
return . visitChildren
225
261
}
226
262
@@ -231,14 +267,14 @@ private final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
231
267
}
232
268
233
269
extension SwiftLanguageService {
234
- public func syntacticDocumentTests( for uri: DocumentURI ) async throws -> [ WorkspaceSymbolItem ] ? {
270
+ public func syntacticDocumentTests( for uri: DocumentURI ) async throws -> [ TestItem ] {
235
271
let snapshot = try documentManager. latestSnapshot ( uri)
236
272
return await SyntacticSwiftXCTestScanner . findTestSymbols ( in: snapshot, syntaxTreeManager: syntaxTreeManager)
237
273
}
238
274
}
239
275
240
276
extension ClangLanguageService {
241
- public func syntacticDocumentTests( for uri: DocumentURI ) async -> [ WorkspaceSymbolItem ] ? {
242
- return nil
277
+ public func syntacticDocumentTests( for uri: DocumentURI ) async -> [ TestItem ] {
278
+ return [ ]
243
279
}
244
280
}
0 commit comments