13
13
/// Represents a source location in a Swift file.
14
14
public struct SourceLocation : Hashable , Codable , CustomDebugStringConvertible {
15
15
16
- /// The UTF-8 byte offset into the file where this location resides.
17
- public let offset : Int
18
-
19
16
/// The line in the file where this location resides. 1-based.
17
+ ///
18
+ /// ### See also
19
+ /// ``SourceLocation/presumedLine``
20
20
public var line : Int
21
21
22
22
/// The UTF-8 byte offset from the beginning of the line where this location
23
23
/// resides. 1-based.
24
24
public let column : Int
25
25
26
+ /// The UTF-8 byte offset into the file where this location resides.
27
+ public let offset : Int
28
+
26
29
/// The file in which this location resides.
30
+ ///
31
+ /// ### See also
32
+ /// ``SourceLocation/presumedFile``
27
33
public let file : String
28
34
35
+ /// The line of this location when respecting `#sourceLocation` directives.
36
+ ///
37
+ /// If the location hasn’t been adjusted using `#sourceLocation` directives,
38
+ /// this is the same as `line`.
39
+ public let presumedLine : Int
40
+
41
+ /// The file in which the the location resides when respecting `#sourceLocation`
42
+ /// directives.
43
+ ///
44
+ /// If the location has been adjusted using `#sourceLocation` directives, this
45
+ /// is the file mentioned in the last `#sourceLocation` directive before this
46
+ /// location, otherwise this is the same as `file`.
47
+ public let presumedFile : String
48
+
29
49
/// Returns the location as `<line>:<column>` for debugging purposes.
30
50
/// Do not rely on this output being stable.
31
51
public var debugDescription : String {
@@ -47,11 +67,26 @@ public struct SourceLocation: Hashable, Codable, CustomDebugStringConvertible {
47
67
/// location in the source file has `offset` 0.
48
68
/// - file: A string describing the name of the file in which this location
49
69
/// is contained.
50
- public init ( line: Int , column: Int , offset: Int , file: String ) {
70
+ /// - presumedLine: If the location has been adjusted using `#sourceLocation`
71
+ /// directives, the adjusted line. If `nil`, this defaults to
72
+ /// `line`.
73
+ /// - presumedFile: If the location has been adjusted using `#sourceLocation`
74
+ /// directives, the adjusted file. If `nil`, this defaults to
75
+ /// `file`.
76
+ public init (
77
+ line: Int ,
78
+ column: Int ,
79
+ offset: Int ,
80
+ file: String ,
81
+ presumedLine: Int ? = nil ,
82
+ presumedFile: String ? = nil
83
+ ) {
51
84
self . line = line
52
85
self . offset = offset
53
86
self . column = column
54
87
self . file = file
88
+ self . presumedLine = presumedLine ?? line
89
+ self . presumedFile = presumedFile ?? file
55
90
}
56
91
}
57
92
@@ -83,18 +118,85 @@ public struct SourceRange: Hashable, Codable, CustomDebugStringConvertible {
83
118
}
84
119
}
85
120
121
+ /// Collects all `PoundSourceLocationSyntax` directives in a file.
122
+ fileprivate class SourceLocationCollector : SyntaxVisitor {
123
+ private var sourceLocationDirectives : [ PoundSourceLocationSyntax ] = [ ]
124
+
125
+ override func visit( _ node: PoundSourceLocationSyntax ) -> SyntaxVisitorContinueKind {
126
+ sourceLocationDirectives. append ( node)
127
+ return . skipChildren
128
+ }
129
+
130
+ static func collectSourceLocations( in tree: some SyntaxProtocol ) -> [ PoundSourceLocationSyntax ] {
131
+ let collector = SourceLocationCollector ( viewMode: . sourceAccurate)
132
+ collector. walk ( tree)
133
+ return collector. sourceLocationDirectives
134
+ }
135
+ }
136
+
137
+ fileprivate struct SourceLocationDirectiveArguments {
138
+ enum Error : Swift . Error , CustomStringConvertible {
139
+ case nonDecimalLineNumber( TokenSyntax )
140
+ case stringInterpolationInFileName( StringLiteralExprSyntax )
141
+
142
+ var description : String {
143
+ switch self {
144
+ case . nonDecimalLineNumber( let token) :
145
+ return " ' \( token. text) ' is not a decimal integer "
146
+ case . stringInterpolationInFileName( let stringLiteral) :
147
+ return " The string literal ' \( stringLiteral) ' contains string interpolation, which is not allowed "
148
+ }
149
+ }
150
+ }
151
+
152
+ /// The `file` argument of the `#sourceLocation` directive.
153
+ let file : String
154
+
155
+ /// The `line` argument of the `#sourceLocation` directive.
156
+ let line : Int
157
+
158
+ init ( _ args: PoundSourceLocationArgsSyntax ) throws {
159
+ guard args. fileName. segments. count == 1 ,
160
+ case . stringSegment( let segment) = args. fileName. segments. first!
161
+ else {
162
+ throw Error . stringInterpolationInFileName ( args. fileName)
163
+ }
164
+ self . file = segment. content. text
165
+ guard let line = Int ( args. lineNumber. text) else {
166
+ throw Error . nonDecimalLineNumber ( args. lineNumber)
167
+ }
168
+ self . line = line
169
+ }
170
+ }
171
+
86
172
/// Converts ``AbsolutePosition``s of syntax nodes to ``SourceLocation``s, and
87
173
/// vice-versa. The ``AbsolutePosition``s must be originating from nodes that are
88
174
/// part of the same tree that was used to initialize this class.
89
175
public final class SourceLocationConverter {
90
- let file : String
176
+ private let file : String
91
177
/// The source of the file, modelled as data so it can contain invalid UTF-8.
92
- let source : [ UInt8 ]
178
+ private let source : [ UInt8 ]
93
179
/// Array of lines and the position at the start of the line.
94
- let lines : [ AbsolutePosition ]
180
+ private let lines : [ AbsolutePosition ]
95
181
/// Position at end of file.
96
- let endOfFile : AbsolutePosition
182
+ private let endOfFile : AbsolutePosition
97
183
184
+ /// The information from all `#sourceLocation` directives in the file
185
+ /// necessary to compute presumed locations.
186
+ ///
187
+ /// - `sourceLine` is the line at which the `#sourceLocation` statement occurs
188
+ /// within the current file.
189
+ /// - `arguments` are the `file` and `line` arguments of the directive or `nil`
190
+ /// if spelled as `#sourceLocation()` to reset the source location directive.
191
+ private var sourceLocationDirectives : [ ( sourceLine: Int , arguments: SourceLocationDirectiveArguments ? ) ] = [ ]
192
+
193
+ /// Create a new ``SourceLocationConverter`` to convert betwen ``AbsolutePosition``
194
+ /// and ``SourceLocation`` in a syntax tree.
195
+ ///
196
+ /// This converter ignores any malformed `#sourceLocation` directives, e.g.
197
+ /// `#sourceLocation` directives with a non-decimal line number or with a file
198
+ /// name that contains string interpolation.
199
+ ///
98
200
/// - Parameters:
99
201
/// - file: The file path associated with the syntax tree.
100
202
/// - tree: The root of the syntax tree to convert positions to line/columns for.
@@ -104,11 +206,29 @@ public final class SourceLocationConverter {
104
206
self . source = tree. syntaxTextBytes
105
207
( self . lines, endOfFile) = computeLines ( tree: Syntax ( tree) )
106
208
precondition ( tree. byteSize == endOfFile. utf8Offset)
209
+
210
+ for directive in SourceLocationCollector . collectSourceLocations ( in: tree) {
211
+ let location = self . physicalLocation ( for: directive. positionAfterSkippingLeadingTrivia)
212
+ if let args = directive. args {
213
+ if let parsedArgs = try ? SourceLocationDirectiveArguments ( args) {
214
+ // Ignore any malformed `#sourceLocation` directives.
215
+ sourceLocationDirectives. append ( ( sourceLine: location. line, arguments: parsedArgs) )
216
+ }
217
+ } else {
218
+ // `#sourceLocation()` without any arguments resets the `#sourceLocation` directive.
219
+ sourceLocationDirectives. append ( ( sourceLine: location. line, arguments: nil ) )
220
+ }
221
+ }
107
222
}
108
223
224
+ /// - Important: This initializer does not take `#sourceLocation` directives
225
+ /// into account and doesn’t produce `presumedFile` and
226
+ /// `presumedLine`.
227
+ ///
109
228
/// - Parameters:
110
229
/// - file: The file path associated with the syntax tree.
111
230
/// - source: The source code to convert positions to line/columns for.
231
+ @available ( * , deprecated, message: " Use init(file:tree:) instead " )
112
232
public init ( file: String , source: String ) {
113
233
self . file = file
114
234
self . source = Array ( source. utf8)
@@ -145,13 +265,40 @@ public final class SourceLocationConverter {
145
265
}
146
266
}
147
267
148
- /// Convert a ``AbsolutePosition`` to a ``SourceLocation``. If the position is
268
+ /// Convert a ``AbsolutePosition`` to a ``SourceLocation``.
269
+ ///
270
+ /// If the position is exceeding the file length then the ``SourceLocation``
271
+ /// for the end of file is returned. If position is negative the location for
272
+ /// start of file is returned.
273
+ public func location( for position: AbsolutePosition ) -> SourceLocation {
274
+ let physicalLocation = physicalLocation ( for: position)
275
+ if let lastSourceLocationDirective = sourceLocationDirectives. last ( where: { $0. sourceLine < physicalLocation. line } ) ,
276
+ let arguments = lastSourceLocationDirective. arguments
277
+ {
278
+ let presumedLine = arguments. line + physicalLocation. line - lastSourceLocationDirective. sourceLine - 1
279
+ return SourceLocation (
280
+ line: physicalLocation. line,
281
+ column: physicalLocation. column,
282
+ offset: physicalLocation. offset,
283
+ file: physicalLocation. file,
284
+ presumedLine: presumedLine,
285
+ presumedFile: arguments. file
286
+ )
287
+ }
288
+
289
+ return physicalLocation
290
+ }
291
+
292
+ /// Compute the location of `position` without taking `#sourceLocation`
293
+ /// directives into account.
294
+ ///
295
+ /// If the position is
149
296
/// exceeding the file length then the ``SourceLocation`` for the end of file
150
297
/// is returned. If position is negative the location for start of file is
151
298
/// returned.
152
- public func location ( for origpos : AbsolutePosition ) -> SourceLocation {
299
+ private func physicalLocation ( for position : AbsolutePosition ) -> SourceLocation {
153
300
// Clamp the given position to the end of file if needed.
154
- let pos = min ( origpos , endOfFile)
301
+ let pos = min ( position , endOfFile)
155
302
if pos. utf8Offset < 0 {
156
303
return SourceLocation ( line: 1 , column: 1 , offset: 0 , file: self . file)
157
304
}
0 commit comments