From 5dc7b487915f616b2db817ce7695e4b12d07d543 Mon Sep 17 00:00:00 2001 From: Alex Skorulis Date: Fri, 2 Aug 2024 13:09:47 +1000 Subject: [PATCH] Add knit directive to handle types marked with @_spi --- .../FunctionCallRegistrationParsing.swift | 3 ++- Sources/KnitCodeGen/KnitDirectives.swift | 23 ++++++++++++++++++- Sources/KnitCodeGen/Registration.swift | 9 ++++++-- .../KnitCodeGen/TypeSafetySourceFile.swift | 5 +++- .../KnitDirectivesTests.swift | 10 ++++++++ .../RegistrationParsingTests.swift | 12 ++++++++++ .../TypeSafetySourceFileTests.swift | 15 ++++++++++++ 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/Sources/KnitCodeGen/FunctionCallRegistrationParsing.swift b/Sources/KnitCodeGen/FunctionCallRegistrationParsing.swift index 0e2f01d..8f18059 100644 --- a/Sources/KnitCodeGen/FunctionCallRegistrationParsing.swift +++ b/Sources/KnitCodeGen/FunctionCallRegistrationParsing.swift @@ -183,7 +183,8 @@ private func makeRegistrationFor( arguments: registrationArguments, concurrencyModifier: concurrencyModifier, getterConfig: getterConfig, - functionName: functionName + functionName: functionName, + spi: directives.spi ?? defaultDirectives.spi ) } diff --git a/Sources/KnitCodeGen/KnitDirectives.swift b/Sources/KnitCodeGen/KnitDirectives.swift index 405bf7e..222fd2a 100644 --- a/Sources/KnitCodeGen/KnitDirectives.swift +++ b/Sources/KnitCodeGen/KnitDirectives.swift @@ -9,15 +9,18 @@ public struct KnitDirectives: Codable, Equatable { var accessLevel: AccessLevel? var getterConfig: Set var moduleName: String? + var spi: String? public init( accessLevel: AccessLevel? = nil, getterConfig: Set = [], - moduleName: String? = nil + moduleName: String? = nil, + spi: String? = nil ) { self.accessLevel = accessLevel self.getterConfig = getterConfig self.moduleName = moduleName + self.spi = spi } private static let directiveMarker = "@knit" @@ -56,6 +59,12 @@ public struct KnitDirectives: Codable, Equatable { for getter in parsed.getterConfig { result.getterConfig.insert(getter) } + if let spi = parsed.spi { + if result.spi != nil { + throw Error.duplicateSPI(name: spi) + } + result.spi = spi + } } return result @@ -85,6 +94,14 @@ public struct KnitDirectives: Codable, Equatable { return KnitDirectives(moduleName: name) } } + if let spiMatch = spiRegex.firstMatch(in: token, range: NSMakeRange(0, token.count)) { + if spiMatch.numberOfRanges >= 2, spiMatch.range(at: 1).location != NSNotFound { + var range = spiMatch.range(at: 1) + range = NSRange(location: range.location + 1, length: range.length - 2) + let spi = (token as NSString).substring(with: range) + return KnitDirectives(spi: spi) + } + } throw Error.unexpectedToken(token: token) } @@ -95,16 +112,20 @@ public struct KnitDirectives: Codable, Equatable { private static let getterNamedRegex = try! NSRegularExpression(pattern: "getter-named(\\(\"\\w*\"\\))?") private static let moduleNameRegex = try! NSRegularExpression(pattern: "module-name(\\(\"\\w*\"\\))") + private static let spiRegex = try! NSRegularExpression(pattern: "@_spi(\\(\\w*\\))") } extension KnitDirectives { enum Error: LocalizedError { case unexpectedToken(token: String) + case duplicateSPI(name: String) var errorDescription: String? { switch self { case let .unexpectedToken(token): return "Unexpected knit comment rule \(token)" + case let .duplicateSPI(name): + return "Duplicate @_spi annotations are not supported" } } } diff --git a/Sources/KnitCodeGen/Registration.swift b/Sources/KnitCodeGen/Registration.swift index 8d4d712..b1d99f4 100644 --- a/Sources/KnitCodeGen/Registration.swift +++ b/Sources/KnitCodeGen/Registration.swift @@ -26,6 +26,9 @@ public struct Registration: Equatable, Codable { /// The Swinject function that was used to register this factory public let functionName: FunctionName + /// System Programming Interface annotation that should be applied to the registration + public var spi: String? + public init( service: String, name: String? = nil, @@ -33,7 +36,8 @@ public struct Registration: Equatable, Codable { arguments: [Argument] = [], concurrencyModifier: String? = nil, getterConfig: Set = GetterConfig.default, - functionName: FunctionName = .register + functionName: FunctionName = .register, + spi: String? = nil ) { self.service = service self.name = name @@ -42,6 +46,7 @@ public struct Registration: Equatable, Codable { self.arguments = arguments self.getterConfig = getterConfig self.functionName = functionName + self.spi = spi } /// This registration is forwarded to another service entry. @@ -51,7 +56,7 @@ public struct Registration: Equatable, Codable { private enum CodingKeys: CodingKey { // ifConfigCondition is not encoded since ExprSyntax does not conform to codable - case service, name, accessLevel, arguments, getterConfig, functionName, concurrencyModifier + case service, name, accessLevel, arguments, getterConfig, functionName, concurrencyModifier, spi } } diff --git a/Sources/KnitCodeGen/TypeSafetySourceFile.swift b/Sources/KnitCodeGen/TypeSafetySourceFile.swift index b570d01..b565ac2 100644 --- a/Sources/KnitCodeGen/TypeSafetySourceFile.swift +++ b/Sources/KnitCodeGen/TypeSafetySourceFile.swift @@ -56,8 +56,11 @@ public enum TypeSafetySourceFile { getterType: GetterConfig = .callAsFunction ) throws -> DeclSyntaxProtocol { var modifier = "" + if let spi = registration.spi { + modifier += "@_spi(\(spi)) " + } if let concurrencyModifier = registration.concurrencyModifier { - modifier = "\(concurrencyModifier) " + modifier += "\(concurrencyModifier) " } modifier += registration.accessLevel == .public ? "public " : "" let nameInput = enumName.map { "name: \($0)" } diff --git a/Tests/KnitCodeGenTests/KnitDirectivesTests.swift b/Tests/KnitCodeGenTests/KnitDirectivesTests.swift index 229153c..2b1dd5a 100644 --- a/Tests/KnitCodeGenTests/KnitDirectivesTests.swift +++ b/Tests/KnitCodeGenTests/KnitDirectivesTests.swift @@ -91,6 +91,16 @@ final class KnitDirectivesTests: XCTestCase { ) } + func testSPI() { + XCTAssertEqual( + try parse("// @knit @_spi(Testing)"), + .init(accessLevel: nil, spi: "Testing") + ) + + // Only 1 SPI is supported, the second will cause the parsing to throw + XCTAssertThrowsError(try parse("// @knit @_spi(First) @_spi(Second)")) + } + func testModuleName() { XCTAssertEqual( try parse("// @knit module-name(\"Test\")"), diff --git a/Tests/KnitCodeGenTests/RegistrationParsingTests.swift b/Tests/KnitCodeGenTests/RegistrationParsingTests.swift index 2888481..0dbd35c 100644 --- a/Tests/KnitCodeGenTests/RegistrationParsingTests.swift +++ b/Tests/KnitCodeGenTests/RegistrationParsingTests.swift @@ -583,6 +583,18 @@ final class RegistrationParsingTests: XCTestCase { ) } + func testSPIParsing() throws { + try assertMultipleRegistrationsString( + """ + // @knit @_spi(Testing) + container.registerAbstract(MyType.self) + """, + registrations: [ + Registration(service: "MyType", functionName: .registerAbstract, spi: "Testing"), + ] + ) + } + func testIncorrectRegistrations() throws { try assertNoRegistrationsString("container.someOtherMethod(AType.self)", message: "Incorrect method name") try assertNoRegistrationsString("container.register(A)", message: "First param is not a metatype") diff --git a/Tests/KnitCodeGenTests/TypeSafetySourceFileTests.swift b/Tests/KnitCodeGenTests/TypeSafetySourceFileTests.swift index ca13a38..a772a31 100644 --- a/Tests/KnitCodeGenTests/TypeSafetySourceFileTests.swift +++ b/Tests/KnitCodeGenTests/TypeSafetySourceFileTests.swift @@ -157,6 +157,21 @@ final class TypeSafetySourceFileTests: XCTestCase { ) } + func testRegistrationWithSPI() { + let registration = Registration(service: "A", accessLevel: .public, spi: "Testing") + XCTAssertEqual( + try TypeSafetySourceFile.makeResolver( + registration: registration, + enumName: nil + ).formatted().description, + """ + @_spi(Testing) public func callAsFunction(file: StaticString = #fileID, function: StaticString = #function, line: UInt = #line) -> A { + knitUnwrap(resolve(A.self), callsiteFile: file, callsiteFunction: function, callsiteLine: line) + } + """ + ) + } + func testArgumentNames() { let registration1 = Registration(service: "A", accessLevel: .public, arguments: [.init(type: "String?")]) XCTAssertEqual(