Skip to content

Commit 24ea1d5

Browse files
authored
Add Accessor control (#21)
This pull request introduces a new way to control access levels for generated parsing and printing extensions in the `@ParseStruct` and `@ParseEnum` macros, allowing users to specify custom access modifiers.
1 parent f0932ab commit 24ea1d5

File tree

17 files changed

+564
-129
lines changed

17 files changed

+564
-129
lines changed

.github/workflows/deploy-docc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ jobs:
2626
xcrun swift build
2727
xcrun swift package --allow-writing-to-directory ./docs \
2828
generate-documentation \
29+
--enable-experimental-combined-documentation \
2930
--target BinaryParseKit \
31+
--target BinaryParseKitCommons \
32+
--target BinaryParsing \
3033
--output-path ./docs \
3134
--transform-for-static-hosting
3235
- name: Upload artifact

Package.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ let package = Package(
2121
dependencies: [
2222
.package(url: "https://github.com/swiftlang/swift-syntax.git", .upToNextMajor(from: "602.0.0")),
2323
.package(url: "https://github.com/FlickerSoul/swift-binary-parsing", branch: "main"),
24-
.package(
25-
url: "https://github.com/apple/swift-collections.git",
26-
.upToNextMinor(from: "1.1.0"),
27-
),
24+
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
2825
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.5"),
2926
.package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.6.4"),
27+
// .package(url: "https://github.com/stackotter/swift-macro-toolkit.git", from: "0.7.2"),
3028
],
3129
targets: [
3230
.macro(
@@ -37,6 +35,7 @@ let package = Package(
3735
.product(name: "BinaryParsing", package: "swift-binary-parsing"),
3836
.product(name: "Collections", package: "swift-collections"),
3937
.target(name: "BinaryParseKitCommons"),
38+
// .product(name: "MacroToolkit", package: "swift-macro-toolkit"),
4039
],
4140
),
4241
.target(
@@ -77,13 +76,13 @@ let package = Package(
7776
"BinaryParseKitMacros",
7877
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
7978
.product(name: "MacroTesting", package: "swift-macro-testing"),
79+
"BinaryParseKitCommons",
8080
],
8181
),
8282
.testTarget(
8383
name: "BinaryParseKitTests",
8484
dependencies: [
8585
"BinaryParseKit",
86-
"BinaryParseKitCommons",
8786
],
8887
),
8988
],

Sources/BinaryParseKit/BinaryParseKit.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public macro parse() = #externalMacro(module: "BinaryParseKitMacros", type: "Emp
6060
/// Use this macro when you need to parse a field with a specific byte order
6161
/// (big-endian or little-endian). The field type must conform to ``EndianParsable``.
6262
///
63-
/// - Parameter endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
63+
/// - Parameter endianness: The byte order to use for parsing (`.big` or `.little`)
6464
///
6565
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
6666
///
@@ -164,7 +164,7 @@ public macro parse(byteCount: ByteCount, endianness: Endianness) = #externalMacr
164164
///
165165
/// - Parameters:
166166
/// - byteCountOf: A KeyPath to another field whose value determines the byte count
167-
/// - endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
167+
/// - endianness: The byte order to use for parsing (`.big` or `.little`)
168168
///
169169
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
170170
///
@@ -219,7 +219,7 @@ public macro parseRest() = #externalMacro(
219219
/// Like `parseRest()`, but applies endianness conversion to the remaining data.
220220
/// The field type must conform to ``EndianSizedParsable``.
221221
///
222-
/// - Parameter endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
222+
/// - Parameter endianness: The byte order to use for parsing (`.big` or `.little`)
223223
///
224224
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
225225
///
@@ -255,8 +255,11 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
255255
/// - Proper error handling for parsing failures
256256
/// - A ``Printable`` conformance
257257
///
258-
/// - Note: All fields except those with accessors (`get` and `set`) must be parsed must be marked with `@parse`
259-
/// variants.
258+
/// - Parameters:
259+
/// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`)
260+
/// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`)
261+
///
262+
/// - Note: All fields except those with accessors (`get` and `set`) must be marked with `@parse` variants.
260263
///
261264
/// Example:
262265
/// ```swift
@@ -278,7 +281,10 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
278281
/// let header = try FileHeader(parsing: data)
279282
/// ```
280283
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
281-
public macro ParseStruct() = #externalMacro(
284+
public macro ParseStruct(
285+
parsingAccessor: ExtensionAccessor = .follow,
286+
printingAccessor: ExtensionAccessor = .follow,
287+
) = #externalMacro(
282288
module: "BinaryParseKitMacros",
283289
type: "ConstructStructParseMacro",
284290
)
@@ -295,12 +301,19 @@ public macro ParseStruct() = #externalMacro(
295301
/// - A ``Parsable`` conformance
296302
/// - A ``Printable`` conformance
297303
///
304+
/// - Parameters:
305+
/// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`)
306+
/// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`)
307+
///
298308
/// - Note: All enum cases must be marked with `@match` variants, which is intentional by design, which I don't think is
299-
/// necessary and is possible to be lifted int the future.
309+
/// necessary and is possible to be lifted in the future.
300310
/// - Note: Only one `@matchDefault` case is allowed per enum, and has to be declared at the end of all other cases.
301311
/// - Note: any `match` macro has to proceed `parse` and `skip` macros.
302312
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
303-
public macro ParseEnum() = #externalMacro(
313+
public macro ParseEnum(
314+
parsingAccessor: ExtensionAccessor = .follow,
315+
printingAccessor: ExtensionAccessor = .follow,
316+
) = #externalMacro(
304317
module: "BinaryParseKitMacros",
305318
type: "ConstructEnumParseMacro",
306319
)

Sources/BinaryParseKit/Documentation.docc/Articles/GetStarted.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dependencies: [
2828

2929
## Parsing
3030

31-
We have two parsing macros: ``ParseStruct()`` and ``ParseEnum()``. They work together with decorative macros such as ``parse()``, ``match()``, ``skip(byteCount:because:)``, etc.
31+
We have two parsing macros: ``ParseStruct(parsingAccessor:printingAccessor:)`` and ``ParseEnum(parsingAccessor:printingAccessor:)``. They work together with decorative macros such as ``parse()``, ``match()``, ``skip(byteCount:because:)``, etc.
3232

3333
### Parse Struct
3434

Sources/BinaryParseKit/Documentation.docc/BinaryParseKit.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ BinaryParseKit provides a convenient and type-safe way to parse binary data in S
1919

2020
### Struct Parsing Macros
2121

22-
- ``ParseStruct()``
22+
- ``ParseStruct(parsingAccessor:printingAccessor:)``
2323
- ``parse()``
2424
- ``parse(byteCount:)``
2525
- ``parse(endianness:)``
@@ -32,7 +32,7 @@ BinaryParseKit provides a convenient and type-safe way to parse binary data in S
3232

3333
### Enum Parsing Macros
3434

35-
- ``ParseEnum()``
35+
- ``ParseEnum(parsingAccessor:printingAccessor:)``
3636
- ``match()``
3737
- ``match(byte:)``
3838
- ``match(bytes:)``
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// ExtensionAccessor.swift
3+
// BinaryParseKit
4+
//
5+
// Created by Larry Zeng on 11/26/25.
6+
//
7+
8+
/// This enum represents the access level of an extension in Swift, with a few extra cases
9+
public enum ExtensionAccessor: ExpressibleByUnicodeScalarLiteral, Sendable, Codable {
10+
public typealias UnicodeScalarLiteralType = String
11+
12+
/// Same as public keyword
13+
case `public`
14+
/// Same as package keyword
15+
case package
16+
/// Same as internal keyword
17+
case `internal`
18+
/// Same as fileprivate keyword
19+
case `fileprivate`
20+
/// Same as private keyword
21+
case `private`
22+
/// This specifier indicates that the extension should follow the access level of the type it extends
23+
case follow
24+
/// Represents an unknown or unsupported access level, which will be raised as macro error.
25+
case unknown(String)
26+
27+
/// All allowed cases for ExtensionAccessor
28+
///
29+
/// This includes all the defined access levels except for the ``ExtensionAccessor/unknown(_:)`` case.
30+
public static var allowedCases: [ExtensionAccessor] {
31+
[.public, .package, .internal, .fileprivate, .private, .follow]
32+
}
33+
34+
/// A textual representation of the access level.
35+
public var description: String {
36+
switch self {
37+
case .public: "public"
38+
case .package: "package"
39+
case .internal: "internal"
40+
case .fileprivate: "fileprivate"
41+
case .private: "private"
42+
case .follow: "follow"
43+
case let .unknown(value): "unknown(\(value))"
44+
}
45+
}
46+
47+
/// Initializes an `ExtensionAccessor` from a unicode scalar literal.
48+
///
49+
/// - Note: It tries to match the provided string with valid levels specified in ``ExtensionAccessor/allowedCases``,
50+
/// or default to ``ExtensionAccessor/unknown(_:)``.
51+
public init(unicodeScalarLiteral value: String) {
52+
self = ExtensionAccessor
53+
.allowedCases
54+
.first { access in
55+
access.description == value
56+
} ?? .unknown(value)
57+
}
58+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// ExtensionAccessor+.swift
3+
// BinaryParseKit
4+
//
5+
// Created by Larry Zeng on 11/26/25.
6+
//
7+
8+
import BinaryParseKitCommons
9+
import SwiftSyntax
10+
11+
extension ExtensionAccessor {
12+
func getAccessorToken(defaultAccessor: TokenKind) -> TokenKind? {
13+
switch self {
14+
case .public: .keyword(.public)
15+
case .package: .keyword(.package)
16+
case .internal: .keyword(.internal)
17+
case .fileprivate: .keyword(.fileprivate)
18+
case .private: .keyword(.private)
19+
case .follow: defaultAccessor
20+
case .unknown: nil
21+
}
22+
}
23+
}

Sources/BinaryParseKitMacros/Macros/ParseEnum/ConsructParseEnumMacro.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import SwiftSyntaxMacros
1313

1414
public struct ConstructEnumParseMacro: ExtensionMacro {
1515
public static func expansion(
16-
of _: SwiftSyntax.AttributeSyntax,
16+
of attributeNode: SwiftSyntax.AttributeSyntax,
1717
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
1818
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
1919
conformingTo _: [SwiftSyntax.TypeSyntax],
@@ -23,6 +23,12 @@ public struct ConstructEnumParseMacro: ExtensionMacro {
2323
throw ParseEnumMacroError.onlyEnumsAreSupported
2424
}
2525

26+
let accessorInfo = try extractAccessor(
27+
from: attributeNode,
28+
attachedTo: enumDeclaration,
29+
in: context,
30+
)
31+
2632
let visitor = ParseEnumCase(context: context)
2733
visitor.walk(enumDeclaration)
2834
try visitor.validate()
@@ -31,12 +37,10 @@ public struct ConstructEnumParseMacro: ExtensionMacro {
3137
throw ParseEnumMacroError.unexpectedError(description: "Macro analysis finished without info")
3238
}
3339

34-
let modifiers = declaration.modifiers
35-
3640
let parsingExtension =
3741
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.parsableProtocol)") {
3842
try InitializerDeclSyntax(
39-
"\(modifiers)init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
43+
"\(accessorInfo.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
4044
) {
4145
for caseParseInfo in parseInfo.caseParseInfo {
4246
let toBeMatched = caseParseInfo.bytesToMatch(of: type)
@@ -102,7 +106,7 @@ public struct ConstructEnumParseMacro: ExtensionMacro {
102106

103107
let printerExtension =
104108
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.printableProtocol)") {
105-
try FunctionDeclSyntax("\(modifiers)func printerIntel() throws -> PrinterIntel") {
109+
try FunctionDeclSyntax("\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel") {
106110
try SwitchExprSyntax("switch self") {
107111
for caseParseInfo in parseInfo.caseParseInfo {
108112
var parseSkipMacroInfo: [PrintableFieldInfo] = []

Sources/BinaryParseKitMacros/Macros/ParseStruct/ConstructParseStructMacro.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,32 @@ import SwiftSyntaxMacros
1111

1212
public struct ConstructStructParseMacro: ExtensionMacro {
1313
public static func expansion(
14-
of _: SwiftSyntax.AttributeSyntax,
14+
of attributeNode: SwiftSyntax.AttributeSyntax,
1515
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
1616
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
1717
conformingTo _: [SwiftSyntax.TypeSyntax],
1818
in context: some SwiftSyntaxMacros.MacroExpansionContext,
1919
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
2020
guard let structDeclaration = declaration.as(StructDeclSyntax.self) else {
21-
let error = ParseStructMacroError.onlyStructsAreSupported
22-
throw error
21+
throw ParseStructMacroError.onlyStructsAreSupported
2322
}
23+
24+
let accessorInfo = try extractAccessor(
25+
from: attributeNode,
26+
attachedTo: structDeclaration,
27+
in: context,
28+
)
29+
2430
let structFieldInfo = ParseStructField(context: context)
2531
structFieldInfo.walk(structDeclaration)
2632
try structFieldInfo.validate(for: structDeclaration)
2733

2834
let type = TypeSyntax(type)
29-
let modifiers = declaration.modifiers
3035

3136
let extensionSyntax =
3237
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.parsableProtocol)") {
3338
try InitializerDeclSyntax(
34-
"\(modifiers)init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
39+
"\(accessorInfo.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
3540
) {
3641
for (variableName, variableInfo) in structFieldInfo.variables {
3742
for action in variableInfo.parseActions {
@@ -53,7 +58,7 @@ public struct ConstructStructParseMacro: ExtensionMacro {
5358

5459
let printerExtension =
5560
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.printableProtocol)") {
56-
try FunctionDeclSyntax("\(modifiers)func printerIntel() throws -> PrinterIntel") {
61+
try FunctionDeclSyntax("\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel") {
5762
var parseSkipMacroInfo: [PrintableFieldInfo] = []
5863

5964
for (variableName, variableInfo) in structFieldInfo.variables {

0 commit comments

Comments
 (0)