Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add knit directive to handle types marked with @_spi #184

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/KnitCodeGen/FunctionCallRegistrationParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ private func makeRegistrationFor(
arguments: registrationArguments,
concurrencyModifier: concurrencyModifier,
getterConfig: getterConfig,
functionName: functionName
functionName: functionName,
spi: directives.spi ?? defaultDirectives.spi
)
}

Expand Down
23 changes: 22 additions & 1 deletion Sources/KnitCodeGen/KnitDirectives.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ public struct KnitDirectives: Codable, Equatable {
var accessLevel: AccessLevel?
var getterConfig: Set<GetterConfig>
var moduleName: String?
var spi: String?

public init(
accessLevel: AccessLevel? = nil,
getterConfig: Set<GetterConfig> = [],
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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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"
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions Sources/KnitCodeGen/Registration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ 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,
accessLevel: AccessLevel = .internal,
arguments: [Argument] = [],
concurrencyModifier: String? = nil,
getterConfig: Set<GetterConfig> = GetterConfig.default,
functionName: FunctionName = .register
functionName: FunctionName = .register,
spi: String? = nil
) {
self.service = service
self.name = name
Expand All @@ -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.
Expand All @@ -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
}

}
Expand Down
5 changes: 4 additions & 1 deletion Sources/KnitCodeGen/TypeSafetySourceFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)" }
Expand Down
10 changes: 10 additions & 0 deletions Tests/KnitCodeGenTests/KnitDirectivesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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\")"),
Expand Down
12 changes: 12 additions & 0 deletions Tests/KnitCodeGenTests/RegistrationParsingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 15 additions & 0 deletions Tests/KnitCodeGenTests/TypeSafetySourceFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down