Skip to content

Commit 22435e4

Browse files
committed
Deprecate ByteSourceRange in favor of Range<AbsolutePosition>
While being a little more verbose, this has a few advantages: - We only have a single type to represent ranges in SwiftSyntax instead of `Range<AbsolutePosition>` and `ByteSourceRange`, both of which we needed to use in sourcekit-lsp - Unifying the convenience functions on the two results in a single type that has all the convenience functions, instead of spreading them to two distinct sets - The use of `AbsolutePosition` and `SourceLength` makes type-system guarantees that these are UTF-8 byte positions / length, making it harder to accidentally add eg. UTF-16 lengths with UTF-8 lengths. rdar://125624626
1 parent cfd0487 commit 22435e4

File tree

11 files changed

+236
-138
lines changed

11 files changed

+236
-138
lines changed

Release Notes/600.md

+31
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
- Description: With the change to parse `#if canImport(MyModule, _version: 1.2.3)` as a function call instead of a dedicated syntax node, `1.2.3` natively gets parsed as a member access `3` to the `1.2` float literal. This property allows the reinterpretation of such an expression as a version tuple.
7676
- Pull request: https://github.com/apple/swift-syntax/pull/2025
7777

78+
- `Range<AbsolutePosition>`
79+
- Description: `Range<AbsolutePosition>` gained a few convenience functions inspirec from `ByteSourceRange`: `init(position:length:)`, `length`, `intersectsOrTouches`, `intersects`, `intersecting`
80+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
81+
7882
## API Behavior Changes
7983

8084
## Deprecations
@@ -108,6 +112,18 @@
108112
- Description: Instead of parsing `canImport` inside `#if` directives as a special expression node, parse it as a functionc call expression. This is in-line with how the `swift(>=6.0)` and `compiler(>=6.0)` directives are parsed.
109113
- Pull request: https://github.com/apple/swift-syntax/pull/2025
110114

115+
- `SyntaxClassifiedRange.offset`, `length` and `endOffset`
116+
- Description: Instead of providing these properties work with the replaced `range` instead.
117+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
118+
119+
- `SyntaxProtocol.totalByteRange` and `trimmedByteRange`
120+
- Description: Renamed to `range` and `trimmedRange` with the deprecation of `ByteSourceRange` in favor of `Range<AbsolutePosition>`
121+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
122+
123+
- `ByteSourceRange` deprecated in favor of `Range<AbsolutePosition>`
124+
- Description: Instead of having a dedicated range type in SwiftSyntax, use `Range<AbsolutePosition>` instead. `Range<AbsolutePosition>` has deprecated compatibility layers to make it API-compatible with `ByteSourceRange`
125+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
126+
111127
## API-Incompatible Changes
112128

113129
- `MacroDefinition` used for expanding macros:
@@ -156,6 +172,21 @@
156172
- Pull request: https://github.com/apple/swift-syntax/pull/2531
157173
- Migration steps: Use `if case .backslash = triviaPiece` instead
158174

175+
- `ByteSourceRange.length` changed from `Int` to `SourceLength`
176+
- Description: `ByteSourceRange` has been deprecated and declared as a typealias for `Range<AbsolutePosition>`. At the same time, `Range<AbsolutePosition>` gained `length: SourceLength` that provides type-system information about the kind of length (UTF-8 byte length).
177+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
178+
179+
- `IncrementalEdit.replacementLength` changed from `Int` to `SourceLength`
180+
- Description: The type of `IncrementalEdit.replacementLength` has been changed from `Int` to `SourceLength` which provides type-system information about the kind of length (UTF-8 byte length).
181+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
182+
183+
## API-Behavior Changes
184+
185+
- `SyntaxProtocol.classifications(in:)` and `SyntaxProtocol.classification(at:)` take positions relative to the root of the syntax tree instead of relative to the start of the node
186+
- Description: With the deprecation of `ByteSourceRange` in favor of `Range<AbsolutePosition>`, the `AbsolutePosition`s passed as the range are measured from the start of the syntax tree instead of the start of the current node.
187+
- Pull request: https://github.com/apple/swift-syntax/pull/2587
188+
- Migration steps: Pass absolute positions measured from the root of the syntax tree instead of positions relative to the current node.
189+
159190
## Template
160191

161192
- *Affected API or two word description*

Sources/SwiftIDEUtils/Syntax+Classifications.swift

+14-14
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@ public extension SyntaxProtocol {
2525
/// consecutive tokens would have the same classification then a single classified
2626
/// range is provided for all of them.
2727
var classifications: SyntaxClassifications {
28-
let fullRange = ByteSourceRange(offset: 0, length: totalLength.utf8Length)
29-
return SyntaxClassifications(_syntaxNode, in: fullRange)
28+
return SyntaxClassifications(_syntaxNode, in: self.range)
3029
}
3130

3231
/// Sequence of ``SyntaxClassifiedRange``s contained in this syntax node within
33-
/// a relative range.
32+
/// a source range.
3433
///
3534
/// The provided classified ranges may extend beyond the provided `range`.
3635
/// Active classifications (non-`none`) will extend the range to include the
@@ -41,30 +40,31 @@ public extension SyntaxProtocol {
4140
/// intersect the provided `range`.
4241
///
4342
/// - Parameters:
44-
/// - in: The relative byte range to pull ``SyntaxClassifiedRange``s from.
43+
/// - in: The range to pull ``SyntaxClassifiedRange``s from.
4544
/// - Returns: Sequence of ``SyntaxClassifiedRange``s.
46-
func classifications(in range: ByteSourceRange) -> SyntaxClassifications {
45+
func classifications(in range: Range<AbsolutePosition>) -> SyntaxClassifications {
4746
return SyntaxClassifications(_syntaxNode, in: range)
4847
}
4948

50-
/// The ``SyntaxClassifiedRange`` for a relative byte offset.
49+
/// The ``SyntaxClassifiedRange`` for an byte offset.
5150
/// - Parameters:
52-
/// - at: The relative to the node byte offset.
53-
/// - Returns: The ``SyntaxClassifiedRange`` for the offset or nil if the source text
51+
/// - at: The byte offset measured relative to the syntax tree's root.
52+
/// - Returns: The ``SyntaxClassifiedRange`` for the offset or `nil`` if the source text
5453
/// at the given offset is unclassified.
54+
@available(*, deprecated, message: "Use classification(at: AbsolutePosition) instead")
5555
func classification(at offset: Int) -> SyntaxClassifiedRange? {
56-
let classifications = SyntaxClassifications(_syntaxNode, in: ByteSourceRange(offset: offset, length: 1))
57-
var iterator = classifications.makeIterator()
58-
return iterator.next()
56+
return classification(at: AbsolutePosition(utf8Offset: offset))
5957
}
6058

6159
/// The ``SyntaxClassifiedRange`` for an absolute position.
6260
/// - Parameters:
6361
/// - at: The absolute position.
64-
/// - Returns: The ``SyntaxClassifiedRange`` for the position or nil if the source text
62+
/// - Returns: The ``SyntaxClassifiedRange`` for the position or `nil`` if the source text
6563
/// at the given position is unclassified.
6664
func classification(at position: AbsolutePosition) -> SyntaxClassifiedRange? {
67-
let relativeOffset = position.utf8Offset - self.position.utf8Offset
68-
return self.classification(at: relativeOffset)
65+
let range = Range(position: position, length: SourceLength(utf8Length: 1))
66+
let classifications = SyntaxClassifications(_syntaxNode, in: range)
67+
var iterator = classifications.makeIterator()
68+
return iterator.next()
6969
}
7070
}

Sources/SwiftIDEUtils/SyntaxClassifier.swift

+27-30
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ extension TokenSyntax {
4444

4545
extension RawTriviaPiece {
4646
func classify(offset: Int) -> SyntaxClassifiedRange {
47-
let range = ByteSourceRange(offset: offset, length: byteLength)
47+
let range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + byteLength)
4848
switch self {
4949
case .lineComment: return .init(kind: .lineComment, range: range)
5050
case .blockComment: return .init(kind: .blockComment, range: range)
@@ -63,7 +63,7 @@ fileprivate struct TokenKindAndText {
6363
offset: Int,
6464
contextualClassification: (SyntaxClassification, Bool)?
6565
) -> SyntaxClassifiedRange {
66-
let range = ByteSourceRange(offset: offset, length: text.count)
66+
let range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + text.count)
6767

6868
if let contextualClassify = contextualClassification {
6969
let (classify, force) = contextualClassify
@@ -91,10 +91,15 @@ fileprivate struct TokenKindAndText {
9191
/// Represents a source range that is associated with a syntax classification.
9292
public struct SyntaxClassifiedRange: Equatable, Sendable {
9393
public var kind: SyntaxClassification
94-
public var range: ByteSourceRange
94+
public var range: Range<AbsolutePosition>
9595

96+
@available(*, deprecated, message: "Use range.lowerBound.utf8Offset instead")
9697
public var offset: Int { return range.offset }
97-
public var length: Int { return range.length }
98+
99+
@available(*, deprecated, message: "Use range.utf8Length instead")
100+
public var length: Int { return range.length.utf8Length }
101+
102+
@available(*, deprecated, message: "Use range.upperBound.utf8Offset instead")
98103
public var endOffset: Int { return range.endOffset }
99104
}
100105

@@ -110,19 +115,14 @@ private struct ClassificationVisitor {
110115
var contextualClassification: (SyntaxClassification, Bool)?
111116
}
112117

113-
/// Only tokens within this absolute range will be classified. No
114-
/// classifications will be reported for tokens out of this range.
115-
private var targetRange: ByteSourceRange
118+
/// Only tokens within this range will be classified.
119+
/// No classifications will be reported for tokens out of this range.
120+
private var targetRange: Range<AbsolutePosition>
116121

117122
var classifications: [SyntaxClassifiedRange]
118123

119-
/// Only classify tokens in `relativeClassificationRange`, where the start
120-
/// offset is relative to `node`.
121-
init(node: Syntax, relativeClassificationRange: ByteSourceRange) {
122-
let range = ByteSourceRange(
123-
offset: node.position.utf8Offset + relativeClassificationRange.offset,
124-
length: relativeClassificationRange.length
125-
)
124+
/// Only classify tokens in `range`.
125+
init(node: Syntax, range: Range<AbsolutePosition>) {
126126
self.targetRange = range
127127
self.classifications = []
128128

@@ -140,24 +140,22 @@ private struct ClassificationVisitor {
140140
}
141141

142142
private mutating func report(range: SyntaxClassifiedRange) {
143-
if range.kind == .none && range.length == 0 {
143+
if range.kind == .none && range.range.isEmpty {
144144
return
145145
}
146146

147147
// Merge consecutive classified ranges of the same kind.
148148
if let last = classifications.last,
149149
last.kind == range.kind,
150-
last.endOffset == range.offset
150+
last.range.upperBound == range.range.lowerBound
151151
{
152-
classifications[classifications.count - 1].range = ByteSourceRange(
153-
offset: last.offset,
154-
length: last.length + range.length
155-
)
152+
classifications[classifications.count - 1].range =
153+
last.range.lowerBound..<(last.range.upperBound + range.range.length)
156154
return
157155
}
158156

159-
guard range.offset <= targetRange.endOffset,
160-
range.endOffset >= targetRange.offset
157+
guard range.range.lowerBound <= targetRange.upperBound,
158+
range.range.upperBound >= targetRange.lowerBound
161159
else {
162160
return
163161
}
@@ -219,10 +217,9 @@ private struct ClassificationVisitor {
219217
let layoutNodeTextLength = child.byteLength - child.leadingTriviaByteLength - child.trailingTriviaByteLength
220218
let range = SyntaxClassifiedRange(
221219
kind: classification.classification,
222-
range: ByteSourceRange(
223-
offset: byteOffset,
224-
length: layoutNodeTextLength
225-
)
220+
range: AbsolutePosition(
221+
utf8Offset: byteOffset
222+
)..<AbsolutePosition(utf8Offset: byteOffset + layoutNodeTextLength)
226223
)
227224
report(range: range)
228225
byteOffset += layoutNodeTextLength
@@ -250,10 +247,10 @@ private struct ClassificationVisitor {
250247
}
251248

252249
private mutating func visit(_ descriptor: ClassificationVisitor.Descriptor) -> VisitResult {
253-
guard descriptor.byteOffset < targetRange.endOffset else {
250+
guard descriptor.byteOffset < targetRange.upperBound.utf8Offset else {
254251
return .break
255252
}
256-
guard descriptor.byteOffset + descriptor.node.byteLength > targetRange.offset else {
253+
guard descriptor.byteOffset + descriptor.node.byteLength > targetRange.lowerBound.utf8Offset else {
257254
return .continue
258255
}
259256
guard SyntaxTreeViewMode.sourceAccurate.shouldTraverse(node: descriptor.node) else {
@@ -273,8 +270,8 @@ public struct SyntaxClassifications: Sequence, Sendable {
273270

274271
var classifications: [SyntaxClassifiedRange]
275272

276-
public init(_ node: Syntax, in relRange: ByteSourceRange) {
277-
let visitor = ClassificationVisitor(node: node, relativeClassificationRange: relRange)
273+
public init(_ node: Syntax, in range: Range<AbsolutePosition>) {
274+
let visitor = ClassificationVisitor(node: node, range: range)
278275
self.classifications = visitor.classifications
279276
}
280277

Sources/SwiftParser/IncrementalParseTransition.swift

+29-25
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension Parser {
2323
}
2424

2525
let currentOffset = self.lexemes.offsetToStart(self.currentToken)
26-
if let node = parseLookup!.lookUp(currentOffset, kind: kind) {
26+
if let node = parseLookup!.lookUp(AbsolutePosition(utf8Offset: currentOffset), kind: kind) {
2727
self.lexemes.advance(by: node.totalLength.utf8Length, currentToken: &self.currentToken)
2828
return node
2929
}
@@ -117,16 +117,15 @@ struct IncrementalParseLookup {
117117
/// has invalidated the previous ``Syntax`` node.
118118
///
119119
/// - Parameters:
120-
/// - offset: The byte offset of the source string that is currently parsed.
120+
/// - position: The position in the source string that is currently parsed.
121121
/// - kind: The `CSyntaxKind` that the parser expects at this position.
122122
/// - Returns: A ``Syntax`` node from the previous parse invocation,
123123
/// representing the contents of this region, if it is still valid
124124
/// to re-use. `nil` otherwise.
125-
fileprivate mutating func lookUp(_ newOffset: Int, kind: SyntaxKind) -> Syntax? {
126-
guard let prevOffset = translateToPreEditOffset(newOffset) else {
125+
fileprivate mutating func lookUp(_ newPosition: AbsolutePosition, kind: SyntaxKind) -> Syntax? {
126+
guard let prevPosition = translateToPreEditPosition(newPosition) else {
127127
return nil
128128
}
129-
let prevPosition = AbsolutePosition(utf8Offset: prevOffset)
130129
let node = cursorLookup(prevPosition: prevPosition, kind: kind)
131130
if let node {
132131
reusedCallback?(node)
@@ -162,7 +161,7 @@ struct IncrementalParseLookup {
162161

163162
// Fast path check: if parser is past all the edits then any matching node
164163
// can be re-used.
165-
if !edits.edits.isEmpty && edits.edits.last!.range.endOffset < node.position.utf8Offset {
164+
if !edits.edits.isEmpty && edits.edits.last!.range.upperBound < node.position {
166165
return true
167166
}
168167

@@ -172,15 +171,12 @@ struct IncrementalParseLookup {
172171
return false
173172
}
174173

175-
let nodeAffectRange = ByteSourceRange(
176-
offset: node.position.utf8Offset,
177-
length: nodeAffectRangeLength
178-
)
174+
let nodeAffectRange = node.position..<node.position.advanced(by: nodeAffectRangeLength)
179175

180176
for edit in edits.edits {
181177
// Check if this node or the trivia of the next node has been edited. If
182178
// it has, we cannot reuse it.
183-
if edit.range.offset > nodeAffectRange.endOffset {
179+
if edit.range.lowerBound > nodeAffectRange.upperBound {
184180
// Remaining edits don't affect the node. (Edits are sorted)
185181
break
186182
}
@@ -192,19 +188,19 @@ struct IncrementalParseLookup {
192188
return true
193189
}
194190

195-
fileprivate func translateToPreEditOffset(_ postEditOffset: Int) -> Int? {
191+
fileprivate func translateToPreEditPosition(_ postEditOffset: AbsolutePosition) -> AbsolutePosition? {
196192
var offset = postEditOffset
197193
for edit in edits.edits {
198-
if edit.range.offset > offset {
194+
if edit.range.lowerBound > offset {
199195
// Remaining edits doesn't affect the position. (Edits are sorted)
200196
break
201197
}
202-
if edit.range.offset + edit.replacementLength > offset {
198+
if edit.range.lowerBound + edit.replacementLength > offset {
203199
// This is a position inserted by the edit, and thus doesn't exist in
204200
// the pre-edit version of the file.
205201
return nil
206202
}
207-
offset = offset - edit.replacementLength + edit.range.length
203+
offset = offset + edit.range.length - edit.replacementLength
208204
}
209205
return offset
210206
}
@@ -354,22 +350,30 @@ public struct ConcurrentEdits: Sendable {
354350
for (index, existingEdit) in concurrentEdits.enumerated() {
355351
if existingEdit.replacementRange.intersectsOrTouches(editToAdd.range) {
356352
let intersectionLength =
357-
existingEdit.replacementRange.intersected(editToAdd.range).length
353+
existingEdit.replacementRange.intersecting(editToAdd.range)?.length ?? SourceLength(utf8Length: 0)
358354
let replacement: [UInt8]
359355
replacement =
360-
existingEdit.replacement.prefix(max(0, editToAdd.offset - existingEdit.replacementRange.offset))
356+
existingEdit.replacement.prefix(
357+
max(0, editToAdd.range.lowerBound.utf8Offset - existingEdit.replacementRange.lowerBound.utf8Offset)
358+
)
361359
+ editToAdd.replacement
362-
+ existingEdit.replacement.suffix(max(0, existingEdit.replacementRange.endOffset - editToAdd.endOffset))
360+
+ existingEdit.replacement.suffix(
361+
max(0, existingEdit.replacementRange.upperBound.utf8Offset - editToAdd.range.upperBound.utf8Offset)
362+
)
363363
editToAdd = IncrementalEdit(
364-
offset: Swift.min(existingEdit.offset, editToAdd.offset),
365-
length: existingEdit.length + editToAdd.length - intersectionLength,
364+
range: Range(
365+
position: Swift.min(existingEdit.range.lowerBound, editToAdd.range.lowerBound),
366+
length: existingEdit.range.length + editToAdd.range.length - intersectionLength
367+
),
366368
replacement: replacement
367369
)
368370
editIndicesMergedWithNewEdit.append(index)
369-
} else if existingEdit.offset < editToAdd.endOffset {
371+
} else if existingEdit.range.lowerBound < editToAdd.range.upperBound {
370372
editToAdd = IncrementalEdit(
371-
offset: editToAdd.offset - existingEdit.replacementLength + existingEdit.length,
372-
length: editToAdd.length,
373+
range: Range(
374+
position: editToAdd.range.lowerBound + existingEdit.range.length - existingEdit.replacementLength,
375+
length: editToAdd.range.length
376+
),
373377
replacement: editToAdd.replacement
374378
)
375379
}
@@ -380,7 +384,7 @@ public struct ConcurrentEdits: Sendable {
380384
}
381385
let insertPos =
382386
concurrentEdits.firstIndex(where: { edit in
383-
editToAdd.endOffset <= edit.offset
387+
editToAdd.range.upperBound <= edit.range.lowerBound
384388
}) ?? concurrentEdits.count
385389
concurrentEdits.insert(editToAdd, at: insertPos)
386390
precondition(ConcurrentEdits.isValidConcurrentEditArray(concurrentEdits))
@@ -397,7 +401,7 @@ public struct ConcurrentEdits: Sendable {
397401
for i in 1..<edits.count {
398402
let prevEdit = edits[i - 1]
399403
let curEdit = edits[i]
400-
if curEdit.range.offset < prevEdit.range.endOffset {
404+
if curEdit.range.lowerBound < prevEdit.range.upperBound {
401405
return false
402406
}
403407
if curEdit.intersectsRange(prevEdit.range) {

0 commit comments

Comments
 (0)