Skip to content

Commit 200ffec

Browse files
committed
[Macros] Introduce a utility to infer nonisolated conformances for macro-expanded code
The combination of isolated conformance inference with global-actor-isolated types has introduced a source compatibility issue with existing macros that produce conformances that are meant to be nonisolated. Introduce a syntactic transform that identifies the presence of nonisolated members and makes the conformances introduced by the enclosing group "nonisolated". For example, an extension macro that expands to: extension C: P { nonisolated func f() { } } will have the macro expansion adjusted to: extension C: nonisolated P { nonisolated func f() { } }
1 parent 3c65036 commit 200ffec

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed

Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_swift_syntax_library(SwiftSyntaxMacroExpansion
88
MacroReplacement.swift
99
MacroSpec.swift
1010
MacroSystem.swift
11+
SyntaxProtocol+NonisolatedConformances.swift
1112
)
1213

1314
target_link_swift_syntax_libraries(SwiftSyntaxMacroExpansion PUBLIC
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file implements inference of "nonisolated" on the conformances that
14+
// occur within macro-expanded code. It's meant to provide source compatibility
15+
//
16+
17+
import SwiftSyntax
18+
19+
extension SyntaxProtocol {
20+
/// Given some Swift syntax that may contain type definitions and extensions,
21+
/// add "nonisolated" to protocol conformances when there are nonisolated
22+
/// members. For example, given:
23+
///
24+
/// extension X: P {
25+
/// nonisolated func f() { }
26+
/// }
27+
///
28+
/// this operation will produce:
29+
///
30+
/// extension X: nonisolated P {
31+
/// nonisolated func f() { }
32+
/// }
33+
@_spi(Testing) @_spi(Compiler)
34+
public func inferNonisolatedConformances() -> Syntax {
35+
let rewriter = NonisolatedConformanceRewriter()
36+
return rewriter.rewrite(self)
37+
}
38+
}
39+
40+
fileprivate class NonisolatedConformanceRewriter: SyntaxRewriter {
41+
override func visitAny(_ node: Syntax) -> Syntax? {
42+
// We only care about decl groups (non-protocol nominal types + extensions)
43+
// that have nonisolated members and an inheritance clause.
44+
guard let declGroup = node.asProtocol(DeclGroupSyntax.self),
45+
!declGroup.is(ProtocolDeclSyntax.self),
46+
declGroup.containsNonisolatedMembers,
47+
let inheritanceClause = declGroup.inheritanceClause
48+
else {
49+
return nil
50+
}
51+
52+
var skipFirst =
53+
declGroup.is(ClassDeclSyntax.self)
54+
|| (declGroup.is(EnumDeclSyntax.self) && inheritanceClause.inheritedTypes.first?.looksLikeEnumRawType ?? false)
55+
let inheritedTypes = inheritanceClause.inheritedTypes.map { inheritedType in
56+
// If there's already a 'nonisolated' or some kind of custom attribute
57+
if inheritedType.type.hasNonisolatedOrCustomAttribute {
58+
return inheritedType
59+
}
60+
61+
if skipFirst {
62+
skipFirst = false
63+
return inheritedType
64+
}
65+
66+
return inheritedType.with(\.type, "nonisolated \(inheritedType.type)")
67+
}
68+
69+
return Syntax(
70+
fromProtocol: declGroup.with(
71+
\.inheritanceClause,
72+
inheritanceClause.with(
73+
\.inheritedTypes,
74+
InheritedTypeListSyntax(inheritedTypes)
75+
)
76+
)
77+
)
78+
}
79+
}
80+
81+
extension TypeSyntax {
82+
/// Determine whether the given type has a 'nonisolated' specifier or a
83+
/// custom attribute (that could be a global actor).
84+
fileprivate var hasNonisolatedOrCustomAttribute: Bool {
85+
var type = self
86+
while let attributedType = type.as(AttributedTypeSyntax.self) {
87+
// nonisolated
88+
let hasNonisolated = attributedType.specifiers.contains { specifier in
89+
if case .nonisolatedTypeSpecifier = specifier {
90+
return true
91+
}
92+
93+
return false
94+
}
95+
if hasNonisolated {
96+
return true
97+
}
98+
99+
// Any attribute will do.
100+
if !attributedType.attributes.isEmpty {
101+
return true
102+
}
103+
104+
type = attributedType.baseType
105+
}
106+
107+
return false
108+
}
109+
}
110+
111+
extension InheritedTypeSyntax {
112+
/// Determine whether this inherited type "looks like" a raw type, e.g.,
113+
/// if it's one of the integer types or String. This can only be an heuristic,
114+
/// because it does not
115+
fileprivate var looksLikeEnumRawType: Bool {
116+
// TODO: We could probably use a utility to syntactically recognize types
117+
// from the
118+
var text = type.trimmed.description[...]
119+
if text.starts(with: "Swift.") {
120+
text = text.dropFirst(6)
121+
}
122+
123+
switch text {
124+
case "Int", "Int8", "Int16", "Int32", "Int64",
125+
"UInt", "UInt8", "UInt16", "UInt32", "UInt64",
126+
"String":
127+
return true
128+
129+
default: return false
130+
}
131+
}
132+
}
133+
extension DeclModifierListSyntax {
134+
/// Whether the modifier list contains "nonisolated".
135+
fileprivate var hasNonisolated: Bool {
136+
contains { $0.name.tokenKind == .keyword(.nonisolated) }
137+
}
138+
}
139+
140+
extension DeclGroupSyntax {
141+
/// Determine whether any of members is marked "nonisolated.
142+
fileprivate var containsNonisolatedMembers: Bool {
143+
memberBlock.members.lazy.map(\.decl).contains {
144+
$0.asProtocol(WithModifiersSyntax.self)?.modifiers.hasNonisolated ?? false
145+
}
146+
}
147+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
@_spi(Testing) import SwiftSyntaxMacroExpansion
15+
import XCTest
16+
import _SwiftSyntaxTestSupport
17+
18+
final class InferNonisolatedConformancesTests: XCTestCase {
19+
func testAddNonisolatedSimple() {
20+
assertInferNonisolatedConformances(
21+
"""
22+
struct MyStruct: P, Q {
23+
nonisolated func f() { }
24+
}
25+
""",
26+
"""
27+
struct MyStruct: nonisolated P, nonisolated Q {
28+
nonisolated func f() { }
29+
}
30+
"""
31+
)
32+
}
33+
34+
func testAddNonisolatedNested() {
35+
assertInferNonisolatedConformances(
36+
"""
37+
extension MyStruct: P, Q {
38+
nonisolated func f() { }
39+
40+
actor Inner: nonisolated R {
41+
nonisolated var value: Int { 0 }
42+
}
43+
}
44+
""",
45+
"""
46+
extension MyStruct: nonisolated P, nonisolated Q {
47+
nonisolated func f() { }
48+
49+
actor Inner: nonisolated R {
50+
nonisolated var value: Int { 0 }
51+
}
52+
}
53+
"""
54+
)
55+
}
56+
57+
func testNoAddWhenNoNonIsolated() {
58+
assertInferNonisolatedConformances(
59+
"""
60+
struct MyStruct: P, Q {
61+
func f() { }
62+
}
63+
""",
64+
"""
65+
struct MyStruct: P, Q {
66+
func f() { }
67+
}
68+
"""
69+
)
70+
}
71+
72+
func testNoAddWhenExplicit() {
73+
assertInferNonisolatedConformances(
74+
"""
75+
struct MyStruct: P, nonisolated Q, @MainActor R, S {
76+
nonisolated func f() { }
77+
}
78+
""",
79+
"""
80+
struct MyStruct: nonisolated P, nonisolated Q, @MainActor R, nonisolated S {
81+
nonisolated func f() { }
82+
}
83+
"""
84+
)
85+
}
86+
87+
func testNoAddHeuristics() {
88+
assertInferNonisolatedConformances(
89+
"""
90+
class MyClass: P, Q {
91+
nonisolated func f() { }
92+
}
93+
""",
94+
"""
95+
class MyClass: P, nonisolated Q {
96+
nonisolated func f() { }
97+
}
98+
"""
99+
)
100+
}
101+
102+
func testNoAddRawType() {
103+
assertInferNonisolatedConformances(
104+
"""
105+
enum MyEnum: Int, Q {
106+
nonisolated func f() { }
107+
}
108+
""",
109+
"""
110+
enum MyEnum: Int, nonisolated Q {
111+
nonisolated func f() { }
112+
}
113+
"""
114+
)
115+
116+
assertInferNonisolatedConformances(
117+
"""
118+
enum MyEnum: P, Q {
119+
nonisolated func f() { }
120+
}
121+
""",
122+
"""
123+
enum MyEnum: nonisolated P, nonisolated Q {
124+
nonisolated func f() { }
125+
}
126+
"""
127+
)
128+
}
129+
130+
func testNoAddProtocol() {
131+
assertInferNonisolatedConformances(
132+
"""
133+
protocol MyProtocol: P, Q {
134+
nonisolated func f() { }
135+
}
136+
""",
137+
"""
138+
protocol MyProtocol: P, Q {
139+
nonisolated func f() { }
140+
}
141+
"""
142+
)
143+
}
144+
}
145+
146+
public func assertInferNonisolatedConformances(
147+
_ original: DeclSyntax,
148+
_ expected: DeclSyntax,
149+
additionalInfo: @autoclosure () -> String? = nil,
150+
file: StaticString = #filePath,
151+
line: UInt = #line
152+
) {
153+
let result = original.inferNonisolatedConformances()
154+
155+
assertStringsEqualWithDiff(
156+
result.description,
157+
expected.description,
158+
file: file,
159+
line: line
160+
)
161+
}

0 commit comments

Comments
 (0)