Skip to content

Commit e07c1bf

Browse files
committed
Add methods from RangeReplaceableCollection to SyntaxCollection
Especially the `+` operators make manipulation of `SyntaxCollection` a lot easier. `SyntaxCollection` doesn’t conform to `RangeReplaceableCollection` because of two reasons: - `RangeReplaceableCollection` assumes that creating a new colleciton and adding all elements from another collection to it results in the same collection. This is not true for `SyntaxCollection` because the new collection wouldn’t have a parent. This causes eg. the default implementation of `filter` to misbehave. - It can’t make the complexity guarantees that `RangeReplaceableCollection` requires, e.g. `append` is `O(tree depth)` instead of `O(1)`. For the same reason, also remove the conformance to `MutableCollection`.
1 parent 5420c38 commit e07c1bf

File tree

4 files changed

+213
-16
lines changed

4 files changed

+213
-16
lines changed

Sources/SwiftSyntax/SyntaxCollection.swift

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
public protocol SyntaxCollection: SyntaxProtocol, BidirectionalCollection, MutableCollection where Element: SyntaxProtocol {
13+
public protocol SyntaxCollection: SyntaxProtocol, BidirectionalCollection where Element: SyntaxProtocol, Index == SyntaxChildrenIndex {
1414
associatedtype Iterator = SyntaxCollectionIterator<Element>
1515

1616
/// The ``SyntaxKind`` of the syntax node that conforms to ``SyntaxCollection``.
@@ -33,6 +33,17 @@ extension SyntaxCollection {
3333
self.init(Syntax(data))!
3434
}
3535

36+
/// Initialize the collection from a collection of the same type.
37+
///
38+
/// - Note: This initializer is needed to improve source compatibility after
39+
/// the `+` operators have been added. Previously, they created an array
40+
/// that was converted to a ``SyntaxCollection`` and now they create a
41+
/// ``SyntaxCollection``, which makes the initializer a no-op.
42+
@available(*, deprecated, message: "Call to initializer is not necessary.")
43+
public init(_ collection: Self) {
44+
self = collection
45+
}
46+
3647
public init<Children: Sequence>(_ children: Children) where Children.Element == Element {
3748
let arena = SyntaxArena()
3849
// Extend the lifetime of children so their arenas don't get destroyed
@@ -208,6 +219,170 @@ public struct SyntaxCollectionIterator<E: SyntaxProtocol>: IteratorProtocol {
208219
}
209220
}
210221

222+
// MARK: Functions analogous to RangeReplaceableCollection
223+
224+
/// Defines functions that are similar to those in `RangeReplaceableCollection`.
225+
///
226+
/// SyntaxCollection doesn’t conform to `RangeReplaceableCollection` because of
227+
/// two reasons:
228+
/// - `RangeReplaceableCollection` assumes that creating a new colleciton and
229+
/// adding all elements from another collection to it results in the same
230+
/// collection. This is not true for `SyntaxCollection` because the new
231+
/// collection wouldn’t have a parent. This causes eg. the default
232+
/// implementation of `filter` to misbehave.
233+
/// - It can’t make the complexity guarantees that `RangeReplaceableCollection`
234+
/// requires, e.g. `append` is `O(tree depth)` instead of `O(1)`.
235+
extension SyntaxCollection {
236+
/// Replace the nodes in `subrange` by `newElements`.
237+
public mutating func replaceSubrange(_ subrange: Range<Self.Index>, with newElements: some Collection<Element>) {
238+
// We only access the raw nodes of `newElements` below.
239+
// Keep `newElements` alive so their arena doesn't get deallocated.
240+
withExtendedLifetime(newElements) {
241+
var newLayout = layoutView.formLayoutArray()
242+
let layoutRangeLowerBound = (subrange.lowerBound.data?.indexInParent).map(Int.init) ?? newLayout.endIndex
243+
let layoutRangeUpperBound = (subrange.upperBound.data?.indexInParent).map(Int.init) ?? newLayout.endIndex
244+
newLayout.replaceSubrange(layoutRangeLowerBound..<layoutRangeUpperBound, with: newElements.map { $0.raw })
245+
self = replacingLayout(newLayout)
246+
}
247+
}
248+
249+
/// Adds an element to the end of the collection.
250+
///
251+
/// - Parameter newElement: The element to append to the collection.
252+
public mutating func append(_ newElement: Element) {
253+
insert(newElement, at: endIndex)
254+
}
255+
256+
/// Adds the elements of a sequence to the end of this collection.
257+
///
258+
/// - Parameter newElements: The elements to append to the collection.
259+
public mutating func append(contentsOf newElements: some Sequence<Element>) {
260+
insert(contentsOf: Array(newElements), at: endIndex)
261+
}
262+
263+
/// Inserts a new element into the collection at the specified position.
264+
///
265+
/// - Parameter newElement: The new element to insert into the collection.
266+
/// - Parameter i: The position at which to insert the new element.
267+
/// `index` must be a valid index into the collection.
268+
public mutating func insert(_ newElement: Element, at i: Index) {
269+
replaceSubrange(i..<i, with: CollectionOfOne(newElement))
270+
}
271+
272+
/// Inserts the elements of a sequence into the collection at the specified
273+
/// position.
274+
///
275+
/// - Parameter newElements: The new elements to insert into the collection.
276+
/// - Parameter i: The position at which to insert the new elements. `index`
277+
/// must be a valid index of the collection.
278+
public mutating func insert(contentsOf newElements: some Collection<Element>, at i: Index) {
279+
replaceSubrange(i..<i, with: newElements)
280+
}
281+
282+
/// Removes and returns the element at the specified position.
283+
///
284+
/// - Parameter position: The position of the element to remove. `position`
285+
/// must be a valid index of the collection that is not equal to the
286+
/// collection's end index.
287+
/// - Returns: The removed element.
288+
@discardableResult
289+
public mutating func remove(at position: Index) -> Element {
290+
precondition(!isEmpty, "Can't remove from an empty collection")
291+
let result: Element = self[position]
292+
replaceSubrange(position..<index(after: position), with: EmptyCollection())
293+
return result
294+
}
295+
296+
/// Removes the elements in the specified subrange from the collection.
297+
///
298+
/// - Parameter bounds: The range of the collection to be removed. The
299+
/// bounds of the range must be valid indices of the collection.
300+
public mutating func removeSubrange(_ bounds: Range<Index>) {
301+
replaceSubrange(bounds, with: EmptyCollection())
302+
}
303+
304+
/// Creates a new collection by concatenating the elements of a collection and
305+
/// a sequence.
306+
///
307+
/// - Parameters:
308+
/// - lhs: A ``SyntaxCollection``
309+
/// - rhs: A collection or finite sequence.
310+
public static func + (lhs: Self, rhs: some Sequence<Element>) -> Self {
311+
var result = lhs
312+
result.append(contentsOf: rhs)
313+
return result
314+
}
315+
316+
/// Creates a new collection by concatenating the elements of a collection and
317+
/// a sequence.
318+
///
319+
/// - Parameters:
320+
/// - lhs: A ``SyntaxCollection``
321+
/// - rhs: A collection or finite sequence.
322+
///
323+
/// - Note: This overload exists to resolve when adding an array to a ``SyntaxCollection``.
324+
public static func + (lhs: Self, rhs: some RangeReplaceableCollection<Element>) -> Self {
325+
var result = lhs
326+
result.append(contentsOf: rhs)
327+
return result
328+
}
329+
330+
/// Creates a new collection by concatenating the elements of a sequence and a
331+
/// collection.
332+
///
333+
/// - Parameters:
334+
/// - lhs: A collection or finite sequence.
335+
/// - rhs: A range-replaceable collection.
336+
public static func + (lhs: some Sequence<Element>, rhs: Self) -> Self {
337+
var result = rhs
338+
result.insert(contentsOf: Array(lhs), at: result.startIndex)
339+
return result
340+
}
341+
342+
/// Creates a new collection by concatenating the elements of a sequence and a
343+
/// collection.
344+
///
345+
/// - Parameters:
346+
/// - lhs: A collection or finite sequence.
347+
/// - rhs: A range-replaceable collection.
348+
///
349+
/// - Note: This overload exists to resolve when adding an array to a ``SyntaxCollection``.
350+
public static func + (lhs: some RangeReplaceableCollection<Element>, rhs: Self) -> Self {
351+
var result = rhs
352+
result.insert(contentsOf: Array(lhs), at: result.startIndex)
353+
return result
354+
}
355+
356+
/// Appends the elements of a sequence to a range-replaceable collection.
357+
///
358+
/// - Parameters:
359+
/// - lhs: The ``SyntaxCollection`` to append to.
360+
/// - rhs: A collection or finite sequence.
361+
///
362+
/// - Note: This will result in an allocation of a copy of this node.
363+
public static func += (lhs: inout Self, rhs: some Sequence<Element>) {
364+
lhs.append(contentsOf: rhs)
365+
}
366+
367+
/// Returns a new ``SyntaxCollection`` that just contains the elements
368+
/// satisfying the given predicate.
369+
///
370+
/// - Parameter isIncluded: A closure that takes an element of the
371+
/// collection as its argument and returns a Boolean value indicating
372+
/// whether the element should be included in the returned collection.
373+
/// - Returns: A ``SyntaxCollection`` of the elements that `isIncluded` allowed.
374+
///
375+
/// - Note: This creates a new ``SyntaxCollection`` node. If the resulting node
376+
/// is not needed (e.g. because it’s only being iterated), convert the
377+
/// ``SyntaxCollection`` to an array first or use the `where` clause in
378+
/// a `for` statement.
379+
public func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> Self {
380+
var result = self
381+
result.replaceSubrange(self.startIndex..<self.endIndex, with: try Array(self).filter(isIncluded))
382+
return result
383+
}
384+
}
385+
211386
/// Conformance to `BidirectionalCollection`.
212387
extension SyntaxCollection {
213388
public func makeIterator() -> SyntaxCollectionIterator<Element> {

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,9 @@ extension MacroApplication {
461461
}
462462
}
463463

464-
// FIXME: Is there a better way to add N members to a decl?
465-
return decl.with(
466-
\.memberBlock,
467-
newMembers.reduce(decl.memberBlock) { partialMembers, newMember in
468-
partialMembers.addMember(.init(decl: newMember))
469-
}
470-
)
464+
var decl = decl
465+
decl.memberBlock.members += newMembers.map { MemberBlockItemSyntax(decl: $0) }
466+
return decl
471467
}
472468

473469
private func expandAttributes(
@@ -497,7 +493,7 @@ extension MacroApplication {
497493
}
498494

499495
let newAttributes = attributes.reduce(attributedDecl.attributes ?? .init([])) {
500-
AttributeListSyntax($0 + [AttributeListSyntax.Element($1)])
496+
$0 + [AttributeListSyntax.Element($1)]
501497
}
502498

503499
let newDecl = attributedDecl.with(\.attributes, newAttributes).as(DeclSyntax.self)!

Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,7 @@ public struct AddCompletionHandler: PeerMacro {
362362
]
363363
newParameterList = FunctionParameterListSyntax(newParameterListElements)
364364
} else {
365-
newParameterList = FunctionParameterListSyntax(
366-
parameterList + [completionHandlerParam]
367-
)
365+
newParameterList = parameterList + [completionHandlerParam]
368366
}
369367

370368
let callArguments: [String] = parameterList.map { param in

Tests/SwiftSyntaxTest/SyntaxCollectionsTests.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class SyntaxCollectionsTests: XCTestCase {
2828
integerLiteralElement(0)
2929
])
3030

31-
let newArrayElementList = ArrayElementListSyntax(arrayElementList + [integerLiteralElement(1)])
31+
let newArrayElementList = arrayElementList + [integerLiteralElement(1)]
3232
XCTAssert(newArrayElementList.kind.isSyntaxCollection)
3333
XCTAssertEqual(newArrayElementList.count, 2)
3434
XCTAssertNotNil(newArrayElementList.child(at: 1))
@@ -41,13 +41,13 @@ public class SyntaxCollectionsTests: XCTestCase {
4141
integerLiteralElement(1)
4242
])
4343

44-
var newArrayElementList = ArrayElementListSyntax([integerLiteralElement(0)] + arrayElementList)
44+
var newArrayElementList = [integerLiteralElement(0)] + arrayElementList
4545

4646
XCTAssertEqual(newArrayElementList.count, 2)
4747
XCTAssertNotNil(newArrayElementList.child(at: 0))
4848
XCTAssertEqual("\(newArrayElementList.child(at: 0)!)", "0")
4949

50-
newArrayElementList = ArrayElementListSyntax(newArrayElementList + [integerLiteralElement(2)])
50+
newArrayElementList += [integerLiteralElement(2)]
5151

5252
XCTAssertEqual(newArrayElementList.count, 3)
5353
XCTAssertNotNil(newArrayElementList.child(at: 2))
@@ -59,7 +59,7 @@ public class SyntaxCollectionsTests: XCTestCase {
5959
integerLiteralElement(1)
6060
])
6161

62-
let newArrayElementList = ArrayElementListSyntax([integerLiteralElement(0)] + arrayElementList)
62+
let newArrayElementList = [integerLiteralElement(0)] + arrayElementList
6363

6464
XCTAssertEqual(newArrayElementList.count, 2)
6565
XCTAssertNotNil(newArrayElementList.child(at: 0))
@@ -146,4 +146,32 @@ public class SyntaxCollectionsTests: XCTestCase {
146146
XCTAssertEqual("\(relems[1])", "1")
147147
XCTAssertEqual("\(relems[0])", "2")
148148
}
149+
150+
public func testFilter() {
151+
let elements = ArrayElementListSyntax([
152+
integerLiteralElement(0),
153+
integerLiteralElement(1),
154+
integerLiteralElement(2),
155+
])
156+
let array = ArrayExprSyntax(elements: elements)
157+
158+
let filteredElements = array.elements.filter { _ in true }
159+
// The filtered sequence should have the `ArrayExprSyntax` as a parent
160+
XCTAssert(filteredElements.parent?.is(ArrayExprSyntax.self) ?? false)
161+
}
162+
163+
public func testAddingSequence() {
164+
let elements = ArrayElementListSyntax([
165+
integerLiteralElement(0),
166+
integerLiteralElement(1),
167+
integerLiteralElement(2),
168+
])
169+
let array = ArrayExprSyntax(elements: elements)
170+
171+
let addedElements: ArrayElementListSyntax = Array([integerLiteralElement(4)]) + array.elements
172+
XCTAssertEqual(addedElements.count, 4)
173+
// If the first collection of + doesn’t have a parent, it doesn’t really
174+
// make sense to inherit the parent from the second collection.
175+
XCTAssert(addedElements.parent?.is(ArrayExprSyntax.self) ?? false)
176+
}
149177
}

0 commit comments

Comments
 (0)