1111//===----------------------------------------------------------------------===//
1212
1313/// 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 {
1815
1916 /// The line in the file where this location resides. 1-based.
17+ ///
18+ /// ### See also
19+ /// ``SourceLocation/presumedLine``
2020 public var line : Int
2121
2222 /// The UTF-8 byte offset from the beginning of the line where this location
2323 /// resides. 1-based.
2424 public let column : Int
2525
26+ /// The UTF-8 byte offset into the file where this location resides.
27+ public let offset : Int
28+
2629 /// The file in which this location resides.
30+ ///
31+ /// ### See also
32+ /// ``SourceLocation/presumedFile``
2733 public let file : String
2834
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
3548
3649 /// Create a new source location at the specified `line` and `column` in `file`.
3750 ///
@@ -47,16 +60,31 @@ public struct SourceLocation: Hashable, Codable, CustomDebugStringConvertible {
4760 /// location in the source file has `offset` 0.
4861 /// - file: A string describing the name of the file in which this location
4962 /// 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+ ) {
5177 self . line = line
5278 self . offset = offset
5379 self . column = column
5480 self . file = file
81+ self . presumedLine = presumedLine ?? line
82+ self . presumedFile = presumedFile ?? file
5583 }
5684}
5785
5886/// Represents a half-open range in a Swift file.
59- public struct SourceRange : Hashable , Codable , CustomDebugStringConvertible {
87+ public struct SourceRange : Hashable , Codable {
6088
6189 /// The beginning location of the source range.
6290 ///
@@ -69,12 +97,6 @@ public struct SourceRange: Hashable, Codable, CustomDebugStringConvertible {
6997 /// ie. this location is not included in the range.
7098 public let end : SourceLocation
7199
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-
78100 /// Construct a new source range, starting at `start` (inclusive) and ending
79101 /// at `end` (exclusive).
80102 public init ( start: SourceLocation , end: SourceLocation ) {
@@ -83,18 +105,85 @@ public struct SourceRange: Hashable, Codable, CustomDebugStringConvertible {
83105 }
84106}
85107
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+
86159/// Converts ``AbsolutePosition``s of syntax nodes to ``SourceLocation``s, and
87160/// vice-versa. The ``AbsolutePosition``s must be originating from nodes that are
88161/// part of the same tree that was used to initialize this class.
89162public final class SourceLocationConverter {
90- let file : String
163+ private let file : String
91164 /// The source of the file, modelled as data so it can contain invalid UTF-8.
92- let source : [ UInt8 ]
165+ private let source : [ UInt8 ]
93166 /// Array of lines and the position at the start of the line.
94- let lines : [ AbsolutePosition ]
167+ private let lines : [ AbsolutePosition ]
95168 /// Position at end of file.
96- let endOfFile : AbsolutePosition
169+ private let endOfFile : AbsolutePosition
97170
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+ ///
98187 /// - Parameters:
99188 /// - file: The file path associated with the syntax tree.
100189 /// - tree: The root of the syntax tree to convert positions to line/columns for.
@@ -104,11 +193,29 @@ public final class SourceLocationConverter {
104193 self . source = tree. syntaxTextBytes
105194 ( self . lines, endOfFile) = computeLines ( tree: Syntax ( tree) )
106195 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+ }
107209 }
108210
211+ /// - Important: This initializer does not take `#sourceLocation` directives
212+ /// into account and doesn’t produce `presumedFile` and
213+ /// `presumedLine`.
214+ ///
109215 /// - Parameters:
110216 /// - file: The file path associated with the syntax tree.
111217 /// - source: The source code to convert positions to line/columns for.
218+ @available ( * , deprecated, message: " Use init(file:tree:) instead " )
112219 public init ( file: String , source: String ) {
113220 self . file = file
114221 self . source = Array ( source. utf8)
@@ -145,13 +252,40 @@ public final class SourceLocationConverter {
145252 }
146253 }
147254
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
149283 /// exceeding the file length then the ``SourceLocation`` for the end of file
150284 /// is returned. If position is negative the location for start of file is
151285 /// returned.
152- public func location ( for origpos : AbsolutePosition ) -> SourceLocation {
286+ private func physicalLocation ( for position : AbsolutePosition ) -> SourceLocation {
153287 // Clamp the given position to the end of file if needed.
154- let pos = min ( origpos , endOfFile)
288+ let pos = min ( position , endOfFile)
155289 if pos. utf8Offset < 0 {
156290 return SourceLocation ( line: 1 , column: 1 , offset: 0 , file: self . file)
157291 }
0 commit comments