Skip to content

Commit dbfaf0c

Browse files
authored
[ST-0020] Promote SourceLocation.filePath to API. (#1472)
This PR formally implements the changes proposed in [ST-0020](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0020-sourcelocation-filepath.md). Resolves rdar://152999195. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent cd26fdb commit dbfaf0c

11 files changed

Lines changed: 165 additions & 26 deletions

File tree

Documentation/ABI/JSON.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ array (also defined as in JSON) whose elements all follow rule `<T>`.
3939
<bool> ::= true | false ; as in JSON
4040
4141
<source-location> ::= {
42-
"fileID": <string>, ; the Swift file ID of the file
42+
["fileID": <string>,] ; the Swift file ID of the file if available, as per
43+
; SE-0274 § "Specification of the #file string format"
44+
"filePath": <string>, ; the compile-time path to the file
4345
"line": <number>,
4446
"column": <number>,
4547
}
@@ -49,7 +51,8 @@ array (also defined as in JSON) whose elements all follow rule `<T>`.
4951
"since1970": <number>, ; floating-point seconds since 1970-01-01 00:00:00 UT
5052
}
5153
52-
<version> ::= "version": 0 ; will be incremented as the format changes
54+
<version> ::= "version": <version-number>
55+
<version-number> ::= 0 | "<version core>" ; as per https://semver.org
5356
```
5457

5558
<!--
@@ -230,3 +233,4 @@ sufficient information to display the event in a human-readable format.
230233
| [ST-0009](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0009-attachments.md#integration-with-supporting-tools) | Added attachments. | 6.2 | `0` |
231234
| [ST-0013](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0013-issue-severity-warning.md#event-stream) | Added test issue severity. | 6.3 | `"6.3"` |
232235
| [ST-0016](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0016-test-cancellation.md#integration-with-supporting-tools) | Added test cancellation. | 6.3 | `"6.3"` |
236+
| [ST-0020](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0020-sourcelocation-filepath.md#detailed-design) | Added `filePath`. | 6.3 | `"6.3"` |

Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ extension ABI {
9696
///
9797
/// - Warning: Source locations at this level of the JSON schema are not yet
9898
/// part of said JSON schema.
99-
var _sourceLocation: SourceLocation?
99+
var _sourceLocation: EncodedSourceLocation<V>?
100100

101101
init?(encoding event: borrowing Event, in eventContext: borrowing Event.Context, messages: borrowing [Event.HumanReadableOutputRecorder.Message]) {
102102
switch event.kind {
@@ -142,14 +142,14 @@ extension ABI {
142142
switch event.kind {
143143
case let .issueRecorded(recordedIssue):
144144
_comments = recordedIssue.comments.map(\.rawValue)
145-
_sourceLocation = recordedIssue.sourceLocation
145+
_sourceLocation = recordedIssue.sourceLocation.map { EncodedSourceLocation(encoding: $0) }
146146
case let .valueAttached(attachment):
147-
_sourceLocation = attachment.sourceLocation
147+
_sourceLocation = EncodedSourceLocation<V>(encoding: attachment.sourceLocation)
148148
case let .testCaseCancelled(skipInfo),
149149
let .testSkipped(skipInfo),
150150
let .testCancelled(skipInfo):
151151
_comments = Array(skipInfo.comment).map(\.rawValue)
152-
_sourceLocation = skipInfo.sourceLocation
152+
_sourceLocation = skipInfo.sourceLocation.map { EncodedSourceLocation(encoding: $0) }
153153
default:
154154
break
155155
}

Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension ABI {
4646
var isKnown: Bool
4747

4848
/// The location in source where this issue occurred, if available.
49-
var sourceLocation: SourceLocation?
49+
var sourceLocation: EncodedSourceLocation<V>?
5050

5151
/// The backtrace where this issue occurred, if available.
5252
///
@@ -61,7 +61,7 @@ extension ABI {
6161
init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) {
6262
// >= v0
6363
isKnown = issue.isKnown
64-
sourceLocation = issue.sourceLocation
64+
sourceLocation = issue.sourceLocation.map { EncodedSourceLocation(encoding: $0) }
6565

6666
// >= v6.3
6767
if V.versionNumber >= ABI.v6_3.versionNumber {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
extension ABI {
12+
/// A type implementing the JSON encoding of ``SourceLocation`` for the ABI
13+
/// entry point and event stream output.
14+
///
15+
/// This type is not part of the public interface of the testing library. It
16+
/// assists in converting values to JSON; clients that consume this JSON are
17+
/// expected to write their own decoders.
18+
struct EncodedSourceLocation<V>: Sendable where V: ABI.Version {
19+
/// See ``SourceLocation`` for a discussion of these properties.
20+
var fileID: String?
21+
var filePath: String?
22+
var _filePath: String?
23+
var line: Int
24+
var column: Int
25+
26+
init(encoding sourceLocation: borrowing SourceLocation) {
27+
fileID = sourceLocation.fileID
28+
29+
// When using the 6.3 schema, don't encode synthesized file IDs.
30+
if V.versionNumber >= ABI.v6_3.versionNumber,
31+
sourceLocation.moduleName == SourceLocation.synthesizedModuleName {
32+
fileID = nil
33+
}
34+
35+
// When using the 6.3 schema, we encode both "filePath" and "_filePath" to
36+
// ease migration for existing tools.
37+
if V.versionNumber >= ABI.v6_3.versionNumber {
38+
filePath = sourceLocation.filePath
39+
}
40+
if V.versionNumber <= ABI.v6_3.versionNumber {
41+
_filePath = sourceLocation.filePath
42+
}
43+
44+
line = sourceLocation.line
45+
column = sourceLocation.column
46+
}
47+
}
48+
}
49+
50+
// MARK: - Codable
51+
52+
extension ABI.EncodedSourceLocation: Codable {}
53+
54+
// MARK: -
55+
56+
extension SourceLocation {
57+
init?<V>(_ sourceLocation: ABI.EncodedSourceLocation<V>) {
58+
let fileID = sourceLocation.fileID
59+
guard let filePath = sourceLocation.filePath ?? sourceLocation._filePath else {
60+
return nil
61+
}
62+
let line = max(1, sourceLocation.line)
63+
let column = max(1, sourceLocation.column)
64+
65+
self.init(fileIDSynthesizingIfNeeded: fileID, filePath: filePath, line: line, column: column)
66+
}
67+
}

Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension ABI {
3838
var displayName: String?
3939

4040
/// The source location of this test.
41-
var sourceLocation: SourceLocation
41+
var sourceLocation: EncodedSourceLocation<V>
4242

4343
/// A type implementing the JSON encoding of ``Test/ID`` for the ABI entry
4444
/// point and event stream output.
@@ -87,7 +87,7 @@ extension ABI {
8787
}
8888
name = test.name
8989
displayName = test.displayName
90-
sourceLocation = test.sourceLocation
90+
sourceLocation = EncodedSourceLocation(encoding: test.sourceLocation)
9191
id = ID(encoding: test.id)
9292

9393
// Experimental fields

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_library(Testing
2121
ABI/Encoded/ABI.EncodedInstant.swift
2222
ABI/Encoded/ABI.EncodedIssue.swift
2323
ABI/Encoded/ABI.EncodedMessage.swift
24+
ABI/Encoded/ABI.EncodedSourceLocation.swift
2425
ABI/Encoded/ABI.EncodedTest.swift
2526
Attachments/Images/AttachableAsImage.swift
2627
Attachments/Images/_AttachableImageWrapper.swift

Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ extension Event.AdvancedConsoleOutputRecorder {
610610
}
611611

612612
// 2. Location
613-
if let sourceLocation = issue.sourceLocation {
613+
if let sourceLocation = issue.sourceLocation.flatMap(SourceLocation.init) {
614614
output += "\n"
615615
output += " Location: \(sourceLocation.fileName):\(sourceLocation.line):\(sourceLocation.column)\n"
616616
}
@@ -731,7 +731,7 @@ extension Event.AdvancedConsoleOutputRecorder {
731731
output += "\(issuePrefix)\(issueTreePrefix)Expectation failed: \(conciseDescription)\n"
732732

733733
// Add concise source location
734-
if let sourceLocation = issue.sourceLocation {
734+
if let sourceLocation = issue.sourceLocation.flatMap(SourceLocation.init) {
735735
let locationPrefix = issuePrefix + (isLastIssue ? " " : "\(_treeVertical) ")
736736
output += "\(locationPrefix)at \(sourceLocation.fileName):\(sourceLocation.line)\n"
737737
}
@@ -952,7 +952,7 @@ extension Event.AdvancedConsoleOutputRecorder {
952952
var fileName = ""
953953
if let issues = context.testData[testID]?.issues,
954954
let firstIssue = issues.first,
955-
let sourceLocation = firstIssue.sourceLocation {
955+
let sourceLocation = firstIssue.sourceLocation.flatMap(SourceLocation.init) {
956956
fileName = sourceLocation.fileName
957957
}
958958

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ extension ExitTest {
10311031
lazy var comments: [Comment] = event._comments?.map(Comment.init(rawValue:)) ?? []
10321032
lazy var sourceContext = SourceContext(
10331033
backtrace: nil, // A backtrace from the child process will have the wrong address space.
1034-
sourceLocation: event._sourceLocation
1034+
sourceLocation: event._sourceLocation.flatMap(SourceLocation.init)
10351035
)
10361036
lazy var skipInfo = SkipInfo(comment: comments.first, sourceContext: sourceContext)
10371037
if let issue = event.issue {
@@ -1059,7 +1059,7 @@ extension ExitTest {
10591059
}
10601060
issueCopy.record()
10611061
} else if let attachment = event.attachment {
1062-
Attachment.record(attachment, sourceLocation: event._sourceLocation!)
1062+
Attachment.record(attachment, sourceLocation: event._sourceLocation.flatMap(SourceLocation.init)!)
10631063
} else if case .testCancelled = event.kind {
10641064
_ = try? Test.cancel(with: skipInfo)
10651065
}

Sources/Testing/SourceAttribution/SourceLocation+Macro.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
///
1313
/// - Returns: The source location at which this macro is applied.
1414
///
15-
/// This macro can be used in place of `#fileID`, `#line`, and `#column` as a
16-
/// default argument to a function. It expands to an instance of
15+
/// You can use this macro in place of `#fileID`, `#filePath`, `#line`, and
16+
/// `#column` as a default argument to a function. It expands to an instance of
1717
/// ``SourceLocation`` referring to the location of the macro invocation itself
1818
/// (similar to how `#fileID` expands to the ID of the file containing the
19-
/// `#fileID` invocation.)
19+
/// `#fileID` invocation).
2020
@freestanding(expression) public macro _sourceLocation() -> SourceLocation = #externalMacro(module: "TestingMacros", type: "SourceLocationMacro")
2121

2222
extension SourceLocation {

Sources/Testing/SourceAttribution/SourceLocation.swift

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ public struct SourceLocation: Sendable {
7171
}
7272

7373
/// The path to the source file.
74-
@_spi(Experimental)
7574
public var filePath: String
7675

7776
/// The line in the source file.
@@ -212,14 +211,61 @@ extension SourceLocation: Codable {
212211

213212
public init(from decoder: any Decoder) throws {
214213
let container = try decoder.container(keyedBy: _CodingKeys.self)
215-
fileID = try container.decode(String.self, forKey: .fileID)
216-
line = try container.decode(Int.self, forKey: .line)
217-
column = try container.decode(Int.self, forKey: .column)
214+
let fileID = try container.decode(String.self, forKey: .fileID)
215+
let line = try container.decode(Int.self, forKey: .line)
216+
let column = try container.decode(Int.self, forKey: .column)
218217

219218
// For simplicity's sake, we won't be picky about which key contains the
220219
// file path.
221-
filePath = try container.decodeIfPresent(String.self, forKey: .filePath)
220+
let filePath = try container.decodeIfPresent(String.self, forKey: .filePath)
222221
?? container.decode(String.self, forKey: ._filePath)
222+
223+
self.init(fileID: fileID, filePath: filePath, line: line, column: column)
224+
}
225+
226+
init(fileIDSynthesizingIfNeeded fileID: String?, filePath: String, line: Int, column: Int) {
227+
// Synthesize the file ID if needed.
228+
let fileID = fileID ?? Self._synthesizeFileID(fromFilePath: filePath)
229+
self.init(fileID: fileID, filePath: filePath, line: line, column: column)
230+
}
231+
232+
/// The name of the ersatz Swift module used for synthesized file IDs.
233+
static var synthesizedModuleName: String {
234+
"__C"
235+
}
236+
237+
/// Synthesize a file ID from the given file path and module name.
238+
///
239+
/// - Parameters:
240+
/// - filePath: The file path.
241+
/// - moduleName: The module name.
242+
///
243+
/// - Returns: A file path constructed from `filePath` and `moduleName`.
244+
private static func _synthesizeFileID(fromFilePath filePath: String, inModuleNamed moduleName: String = synthesizedModuleName) -> String {
245+
let fileName: String? = {
246+
var filePath = filePath[...]
247+
248+
#if os(Windows)
249+
// On Windows, replace backslashes in the path with slashes. (This is an
250+
// admittedly naïve approach, but this function is not a hot path.)
251+
do {
252+
let characters = filePath.map { $0 == #"\"# ? "/" : $0 }
253+
filePath = String(characters)[...]
254+
}
255+
#endif
256+
257+
// Trim any trailing slashes, then take the substring following the last
258+
// (remaining) slash, if any.
259+
if let lastNonSlashCharacter = filePath.lastIndex(where: { $0 != "/" }) {
260+
filePath = filePath[...lastNonSlashCharacter]
261+
if let lastSlashCharacter = filePath.lastIndex(of: "/") {
262+
filePath = filePath[lastSlashCharacter...].dropFirst()
263+
}
264+
return String(filePath)
265+
}
266+
return nil
267+
}()
268+
return "\(moduleName)/\(fileName ?? filePath)"
223269
}
224270
}
225271

@@ -231,7 +277,7 @@ extension SourceLocation {
231277
/// - Warning: This property is provided temporarily to aid in integrating the
232278
/// testing library with existing tools such as Swift Package Manager. It
233279
/// will be removed in a future release.
234-
@available(swift, deprecated: 100000.0, renamed: "filePath")
280+
@available(swift, deprecated: 6.3, renamed: "filePath")
235281
public var _filePath: String {
236282
get {
237283
filePath

0 commit comments

Comments
 (0)