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