Skip to content

Commit de49d68

Browse files
committed
add swift-function-caller-generator
This helper utility takes a module interface as input, and emits a Swift file importing the module and calling every function in the module. It's intended for testing of safe interop wrappers to make sure they go through the entire pipeline of the compiler instead of succumbing to laziness.
1 parent fa27057 commit de49d68

File tree

6 files changed

+326
-1
lines changed

6 files changed

+326
-1
lines changed

test/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ function(get_test_dependencies SDK result_var_name)
104104
endif()
105105

106106
if(SWIFT_BUILD_SWIFT_SYNTAX)
107-
list(APPEND deps_binaries swift-plugin-server)
107+
list(APPEND deps_binaries
108+
swift-plugin-server
109+
swift-function-caller-generator)
108110
endif()
109111
endif()
110112

test/lit.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ config.wasm_ld = inferSwiftBinary('wasm-ld')
362362
config.swift_plugin_server = inferSwiftBinary('swift-plugin-server')
363363
config.swift_parse_test = inferSwiftBinary('swift-parse-test')
364364
config.swift_scan_test = inferSwiftBinary('swift-scan-test')
365+
config.swift_function_caller_generator = inferSwiftBinary('swift-function-caller-generator')
365366

366367
config.swift_utils = make_path(config.swift_src_root, 'utils')
367368
config.line_directive = make_path(config.swift_utils, 'line-directive')
@@ -759,6 +760,7 @@ config.substitutions.append( ('%swift-parse-test', config.swift_parse_test) )
759760
config.substitutions.append( ('%swift-scan-test', config.swift_scan_test) )
760761
config.substitutions.append( ('%swift-symbolgraph-extract', config.swift_symbolgraph_extract) )
761762
config.substitutions.append( ('%swift-synthesize-interface', config.swift_synthesize_interface) )
763+
config.substitutions.append( ('%swift-function-caller-generator', config.swift_function_caller_generator) )
762764
config.substitutions.append( ('%validate-json', f"{config.python} -m json.tool") )
763765

764766
config.clang_include_dir = make_path(config.llvm_obj_root, 'include')

tools/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ add_swift_tool_subdirectory(libStaticMirror)
2727
add_swift_tool_subdirectory(libMockPlugin)
2828
add_swift_tool_subdirectory(swift-plugin-server)
2929
add_swift_tool_subdirectory(swift-scan-test)
30+
add_swift_tool_subdirectory(swift-function-caller-generator)
3031

3132
if(SWIFT_INCLUDE_TESTS OR SWIFT_INCLUDE_TEST_BINARIES)
3233
add_swift_tool_subdirectory(swift-ide-test)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
if (SWIFT_BUILD_SWIFT_SYNTAX)
2+
add_pure_swift_host_tool(swift-function-caller-generator
3+
Sources/swift-function-caller-generator/swift-function-caller-generator.swift
4+
SWIFT_COMPONENT
5+
testsuite-tools
6+
SWIFT_DEPENDENCIES
7+
SwiftSyntax
8+
SwiftSyntaxBuilder
9+
SwiftSyntaxMacros
10+
PACKAGE_NAME Toolchain
11+
)
12+
endif()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 6.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-function-caller-generator",
7+
platforms: [.macOS(.v11)],
8+
products: [
9+
.executable(name: "swift-function-caller-generator", targets: ["swift-function-caller-generator"]),
10+
],
11+
dependencies: [
12+
.package(path: "../../../swift-syntax")
13+
],
14+
targets: [
15+
.executableTarget(
16+
name: "swift-function-caller-generator",
17+
dependencies: [
18+
.product(name: "SwiftSyntax", package: "swift-syntax"),
19+
.product(name: "SwiftParser", package: "swift-syntax"),
20+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
21+
]
22+
),
23+
],
24+
)
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import Foundation
2+
import SwiftParser
3+
import SwiftSyntax
4+
import SwiftSyntaxMacros
5+
6+
@main
7+
class SwiftMacroTestGen: SyntaxVisitor {
8+
static func main() {
9+
if CommandLine.argc < 2 {
10+
print("error: missing module name (passed 1 argument, expected 2)")
11+
exit(1)
12+
}
13+
let contents =
14+
if CommandLine.argc > 2 {
15+
read(file: CommandLine.arguments[2])
16+
} else {
17+
readStdin()
18+
}
19+
let syntaxTree = Parser.parse(source: contents)
20+
print("import \(CommandLine.arguments[1])\n")
21+
let visitor = SwiftMacroTestGen(viewMode: .all)
22+
visitor.walk(syntaxTree)
23+
}
24+
25+
var typeAlias: [String: TypeSyntax] = [:]
26+
override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
27+
let typeAliasName = node.name.trimmedDescription
28+
let rhsType = node.initializer.value
29+
typeAlias[typeAliasName] = rhsType
30+
return .skipChildren
31+
}
32+
33+
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
34+
var res = node
35+
if res.attributes.contains(where: { $0.isObsolete }) {
36+
// don't try to call the old name of a renamed function
37+
return .skipChildren
38+
}
39+
let surroundingType = getParentType(res)
40+
let selfParam = surroundingType.map { _ in TokenSyntax("self") }
41+
res = createFunctionSignature(res)
42+
res =
43+
res
44+
.with(\.body, createBody(res, selfParam: selfParam))
45+
.with(\.name, "call_\(res.name.withoutBackticks)")
46+
if let surroundingType {
47+
res =
48+
res
49+
.with(
50+
\.signature.parameterClause.parameters,
51+
addSelfParam(
52+
res.signature.parameterClause.parameters, surroundingType, selfParam!)
53+
)
54+
.with(\.leadingTrivia, "\n")
55+
}
56+
print(res)
57+
return .skipChildren
58+
}
59+
60+
func createFunctionSignature(_ f: FunctionDeclSyntax) -> FunctionDeclSyntax {
61+
let params = f.signature.parameterClause.parameters
62+
let funcName = f.name.withoutBackticks.trimmed.text
63+
let newParams = params.enumerated().map { (i, param: FunctionParameterSyntax) in
64+
let paramName = param.name.trimmed.text
65+
var newParam = param
66+
if paramName == "_" || paramName == funcName || "`\(paramName)`" == funcName {
67+
let secondName = TokenSyntax("_\(raw: funcName)_param\(raw: i)").with(
68+
\.leadingTrivia, " ")
69+
let firstName = newParam.firstName
70+
newParam = newParam.with(\.secondName, secondName)
71+
.with(\.firstName, firstName)
72+
}
73+
// compiler warns if "var" or "let" are used as parameter labels unescaped
74+
if newParam.firstName.trimmedDescription == "var"
75+
|| newParam.firstName.trimmedDescription == "let"
76+
{
77+
let firstName = newParam.firstName.escaped
78+
newParam = newParam.with(\.firstName, firstName)
79+
}
80+
// replace type aliases with the concrete type so that `hasUnsafeType` can inspect
81+
// whether we need to add `unsafe`
82+
newParam = newParam.with(\.type, TypeAliasReplacer(typeAlias).visit(newParam.type))
83+
return newParam
84+
}
85+
return f.with(
86+
\.signature.parameterClause.parameters, FunctionParameterListSyntax(newParams))
87+
}
88+
}
89+
90+
class TypeAliasReplacer: SyntaxRewriter {
91+
let typeAlias: [String: TypeSyntax]
92+
init(_ typeAlias: [String: TypeSyntax]) {
93+
self.typeAlias = typeAlias
94+
}
95+
override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax {
96+
if let newType = typeAlias[node.name.trimmedDescription] {
97+
return newType
98+
}
99+
return TypeSyntax(node)
100+
}
101+
}
102+
103+
func read(file path: String) -> String {
104+
do {
105+
return try String(contentsOfFile: path, encoding: .utf8)
106+
} catch {
107+
print("Error reading file \(path): \(error.localizedDescription)")
108+
exit(1)
109+
}
110+
}
111+
112+
func readStdin() -> String {
113+
if let data = try? FileHandle.standardInput.readToEnd(),
114+
let input = String(data: data, encoding: .utf8)
115+
{
116+
return input
117+
} else {
118+
print("Error reading stdin)")
119+
exit(1)
120+
}
121+
}
122+
123+
func createBody(_ f: FunctionDeclSyntax, selfParam: TokenSyntax?) -> CodeBlockSyntax {
124+
var call = createCall(f)
125+
if let selfParam {
126+
call = "\(selfParam).\(call)"
127+
}
128+
return
129+
"""
130+
{
131+
return \(call)
132+
}
133+
"""
134+
}
135+
136+
func createCall(_ f: FunctionDeclSyntax) -> ExprSyntax {
137+
let args = f.signature.parameterClause.parameters.map { param in
138+
var declRef = ExprSyntax(DeclReferenceExprSyntax(baseName: param.name.escapeIfNeeded))
139+
if param.type.isInout {
140+
declRef = "&\(declRef)"
141+
}
142+
return declRef
143+
}
144+
let labels: [TokenSyntax?] = f.signature.parameterClause.parameters.map { param in
145+
let firstName = param.firstName.trimmed
146+
if firstName.text == "_" {
147+
return nil
148+
}
149+
return firstName
150+
}
151+
let labeledArgs: [LabeledExprSyntax] = zip(labels, args).enumerated().map { (i, e) in
152+
let (label, arg) = e
153+
let comma: TokenSyntax? = i < args.count - 1 ? .commaToken(trailingTrivia: " ") : nil
154+
let colon: TokenSyntax? = label != nil ? .colonToken(trailingTrivia: " ") : nil
155+
return LabeledExprSyntax(
156+
label: label?.withoutBackticks, colon: colon, expression: arg, trailingComma: comma)
157+
}
158+
let unsafeKw = hasUnsafeType(f) ? "unsafe " : ""
159+
return ExprSyntax("\(raw: unsafeKw)\(f.name)(\(LabeledExprListSyntax(labeledArgs)))")
160+
}
161+
162+
func hasUnsafeType(_ f: FunctionDeclSyntax) -> Bool {
163+
if f.signature.returnClause?.type.isUnsafe ?? false {
164+
return true
165+
}
166+
return f.signature.parameterClause.parameters.contains(where: { $0.type.isUnsafe })
167+
}
168+
169+
extension TypeSyntax {
170+
var isUnsafe: Bool {
171+
if self.description.contains("Unsafe") {
172+
return true
173+
}
174+
if self.description.contains("OpaquePointer") {
175+
return true
176+
}
177+
return false
178+
}
179+
180+
var isInout: Bool {
181+
guard let attr = self.as(AttributedTypeSyntax.self) else {
182+
return false
183+
}
184+
return attr.specifiers.contains(where: { e in
185+
guard let simpleSpec = e.as(SimpleTypeSpecifierSyntax.self) else {
186+
return false
187+
}
188+
return simpleSpec.specifier.text == "inout"
189+
})
190+
}
191+
}
192+
193+
func addSelfParam(_ params: FunctionParameterListSyntax, _ type: TokenSyntax, _ name: TokenSyntax)
194+
-> FunctionParameterListSyntax
195+
{
196+
return [FunctionParameterSyntax("_ \(name): \(type.trimmed), ")] + params
197+
}
198+
199+
func getParentType(_ node: some SyntaxProtocol) -> TokenSyntax? {
200+
guard let parent = node.parent else {
201+
return nil
202+
}
203+
if let structType = parent.as(StructDeclSyntax.self) {
204+
return structType.name
205+
}
206+
if let classType = parent.as(ClassDeclSyntax.self) {
207+
return classType.name
208+
}
209+
return getParentType(parent)
210+
}
211+
212+
extension FunctionParameterSyntax {
213+
var name: TokenSyntax {
214+
self.secondName ?? self.firstName
215+
}
216+
}
217+
218+
enum TokenEscapeContext {
219+
case declRef
220+
case label
221+
}
222+
223+
extension TokenSyntax {
224+
var withoutBackticks: TokenSyntax {
225+
if self.identifier == nil {
226+
return self
227+
}
228+
return .identifier(self.identifier!.name)
229+
}
230+
var escaped: TokenSyntax {
231+
return self.copyTrivia(to: "`\(raw: self.trimmed.text)`")
232+
}
233+
var escapeIfNeeded: TokenSyntax {
234+
var parser = Parser("let \(self)")
235+
let decl = DeclSyntax.parse(from: &parser)
236+
if !decl.hasError {
237+
return self
238+
} else {
239+
return self.escaped
240+
}
241+
}
242+
243+
func copyTrivia(to other: TokenSyntax) -> TokenSyntax {
244+
return .identifier(
245+
other.text, leadingTrivia: self.leadingTrivia, trailingTrivia: self.trailingTrivia)
246+
}
247+
}
248+
249+
extension Optional {
250+
var asList: [Wrapped] {
251+
if let self {
252+
return [self]
253+
} else {
254+
return []
255+
}
256+
}
257+
}
258+
259+
extension AttributeSyntax {
260+
var isObsolete: Bool {
261+
guard self.attributeName.trimmed.description == "available" else {
262+
return false
263+
}
264+
guard let args = self.arguments else {
265+
return false
266+
}
267+
return switch args {
268+
case .availability(let list):
269+
list.contains(where: {
270+
$0.argument.as(AvailabilityLabeledArgumentSyntax.self)?.label.trimmed.description
271+
== "obsoleted"
272+
})
273+
default: false
274+
}
275+
}
276+
}
277+
extension AttributeListSyntax.Element {
278+
var isObsolete: Bool {
279+
switch self {
280+
case .attribute(let a): return a.isObsolete
281+
case .ifConfigDecl: return false
282+
}
283+
}
284+
}

0 commit comments

Comments
 (0)