From 9f5ce93b07b6121412461eae588f23780e112efa Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Thu, 8 Jan 2026 02:01:31 -0700 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Scribable=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Package.resolved | 15 + Package.swift | 14 +- Sources/Scribe/Log+StaticMethods.swift | 436 ++++++++++++++++++++++ Sources/Scribe/Log.swift | 290 ++++---------- Sources/Scribe/Macros.swift | 110 ++++++ Sources/ScribeMacros/ScribableMacro.swift | 129 +++++++ 6 files changed, 776 insertions(+), 218 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/Scribe/Log+StaticMethods.swift create mode 100644 Sources/Scribe/Macros.swift create mode 100644 Sources/ScribeMacros/ScribableMacro.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..067bbd4 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "17b7149414687671044398e9ba80abb41bea4bc04f2d6ea3ca1b5adbe55af6c1", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 14db3b3..1b13f34 100755 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,7 @@ // swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. +import CompilerPluginSupport import PackageDescription let package = Package( @@ -19,11 +20,22 @@ let package = Package( targets: ["Scribe"] ) ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0-latest") + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. + .macro( + name: "ScribeMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), .target( - name: "Scribe" + name: "Scribe", + dependencies: ["ScribeMacros"] ), .testTarget( name: "ScribeTests", diff --git a/Sources/Scribe/Log+StaticMethods.swift b/Sources/Scribe/Log+StaticMethods.swift new file mode 100644 index 0000000..7c9b694 --- /dev/null +++ b/Sources/Scribe/Log+StaticMethods.swift @@ -0,0 +1,436 @@ +// +// Log+StaticMethods.swift +// Scribe +// +// Created by Kami on 04/02/2025. +// + +import Foundation + +/// Static logging convenience methods providing easy access for all log levels. +/// +/// Use the static methods on this enum to log messages throughout your application. +/// Each method automatically captures source location metadata (file, function, line). +/// +/// ```swift +/// Log.info("User signed in", category: .init("Auth")) +/// Log.error("Failed to fetch data", category: .init("Network")) +/// Log.debug("Processing item \(item.id)") +/// ``` +/// +/// The `category` parameter defaults to the file ID where the log was called, +/// but you can specify custom categories for better filtering and organization. +public extension Log { + // MARK: - Debug & Development + + /// Logs a debug message for development and troubleshooting. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func debug( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .debug, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a trace message for detailed execution flow tracking. + /// + /// Use for granular debugging that's typically too verbose for regular debug builds. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func trace( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .trace, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a print-level message as a structured replacement for `Swift.print()`. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func print( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .print, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - General Information + + /// Logs an informational message about general app state or events. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func info( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .info, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a notice about a notable but non-critical event. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func notice( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .notice, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Warnings & Errors + + /// Logs a warning about a potential issue that doesn't prevent operation. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func warn( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .warning, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs an error that occurred but was handled. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func error( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .error, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a fatal error that may cause app termination or severe malfunction. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func fatal( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .fatal, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Success & Completion + + /// Logs a successful operation or outcome. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func success( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .success, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs task or operation completion. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func done( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .done, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Network Operations + + /// Logs network-related activity. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func network( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .network, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs API call activity (requests, responses, errors). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func api( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .api, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Security & Authentication + + /// Logs security-related events (encryption, permissions, access control). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func security( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .security, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs authentication events (login, logout, token refresh). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func auth( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .auth, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Performance & Analytics + + /// Logs performance metrics (timing, memory, resource usage). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func metric( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .metric, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs analytics events (user behavior, feature usage). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func analytics( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .analytics, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - UI & User Interaction + + /// Logs UI events (view lifecycle, layout, rendering). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func ui( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .ui, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs user actions (taps, gestures, input). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func user( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .user, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Database & Storage + + /// Logs database operations (queries, transactions, migrations). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func database( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .database, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs storage operations (file I/O, cache, persistence). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func storage( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .storage, category: category, fileID: fileID, function: function, file: file, line: line) + } + + private static func log( + _ message: String, + level: LogLevel, + category: LogCategory?, + fileID: String, + function: String, + file: String, + line: Int + ) { + let resolvedCategory = category ?? LogCategory(fileID, isAutoGenerated: true) + logger.log(message, level: level, category: resolvedCategory, file: file, function: function, line: line) + } +} diff --git a/Sources/Scribe/Log.swift b/Sources/Scribe/Log.swift index fe90cd2..5b1fcff 100644 --- a/Sources/Scribe/Log.swift +++ b/Sources/Scribe/Log.swift @@ -2,28 +2,44 @@ // Log.swift // Scribe // -// Created by Kami on 04/02/2025. +// Created by Kai Azim on 2026-01-08. // -import Foundation - -/// Static logging interface providing convenient methods for all log levels. +/// Logging interface providing convenient methods for all log levels. /// -/// Use the static methods on this enum to log messages throughout your application. -/// Each method automatically captures source location metadata (file, function, line). +/// Use the methods on this struct to log messages throughout your category. +/// This is useful if you want to use a shared logger specifically for a class, struct or enum where writing `category: +/// ...` in `Log.` would be repetitive. +/// Similar to ``Log``, Each method automatically captures source location metadata (file, function, line). /// /// ```swift -/// Log.info("User signed in", category: .init("Auth")) -/// Log.error("Failed to fetch data", category: .init("Network")) -/// Log.debug("Processing item \(item.id)") +/// let log = Log(category: .init("Auth")) +/// log.info("User signed in") +/// log.error("User session expired") +/// log.debug("New user registered") +/// ``` +/// +/// It is recommended to use this alongside the `@Scribable` macro, which automatically creates the `Log` for you. +/// You can use is as shown below: +/// +/// ```swift +/// @Scribable +/// struct TokenManager { +/// func test() { +/// log.success("New token generated") +/// } +/// } /// ``` /// -/// The `category` parameter defaults to the file ID where the log was called, -/// but you can specify custom categories for better filtering and organization. @frozen -public enum Log: Sendable { +public struct Log: Sendable { /// The shared `LogManager` instance used by all logging methods. public static var logger: LogManager { LogManager.shared } + public let category: LogCategory + + public init(category: LogCategory) { + self.category = category + } // MARK: - Debug & Development @@ -31,19 +47,11 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func debug( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .debug, category: category, fileID: fileID, function: function, file: file, line: line) + public func debug(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .debug, category: category, function: function, file: file, line: line) } /// Logs a trace message for detailed execution flow tracking. @@ -52,38 +60,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func trace( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .trace, category: category, fileID: fileID, function: function, file: file, line: line) + public func trace(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .trace, category: category, function: function, file: file, line: line) } /// Logs a print-level message as a structured replacement for `Swift.print()`. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func print( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .print, category: category, fileID: fileID, function: function, file: file, line: line) + public func print(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .print, category: category, function: function, file: file, line: line) } // MARK: - General Information @@ -92,38 +84,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func info( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .info, category: category, fileID: fileID, function: function, file: file, line: line) + public func info(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .info, category: category, function: function, file: file, line: line) } /// Logs a notice about a notable but non-critical event. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func notice( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .notice, category: category, fileID: fileID, function: function, file: file, line: line) + public func notice(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .notice, category: category, function: function, file: file, line: line) } // MARK: - Warnings & Errors @@ -132,57 +108,33 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func warn( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .warning, category: category, fileID: fileID, function: function, file: file, line: line) + public func warn(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .warning, category: category, function: function, file: file, line: line) } /// Logs an error that occurred but was handled. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func error( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .error, category: category, fileID: fileID, function: function, file: file, line: line) + public func error(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .error, category: category, function: function, file: file, line: line) } /// Logs a fatal error that may cause app termination or severe malfunction. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func fatal( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .fatal, category: category, fileID: fileID, function: function, file: file, line: line) + public func fatal(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .fatal, category: category, function: function, file: file, line: line) } // MARK: - Success & Completion @@ -191,38 +143,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func success( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .success, category: category, fileID: fileID, function: function, file: file, line: line) + public func success(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .success, category: category, function: function, file: file, line: line) } /// Logs task or operation completion. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func done( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .done, category: category, fileID: fileID, function: function, file: file, line: line) + public func done(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .done, category: category, function: function, file: file, line: line) } // MARK: - Network Operations @@ -231,38 +167,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func network( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .network, category: category, fileID: fileID, function: function, file: file, line: line) + public func network(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .network, category: category, function: function, file: file, line: line) } /// Logs API call activity (requests, responses, errors). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func api( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .api, category: category, fileID: fileID, function: function, file: file, line: line) + public func api(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .api, category: category, function: function, file: file, line: line) } // MARK: - Security & Authentication @@ -271,38 +191,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func security( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .security, category: category, fileID: fileID, function: function, file: file, line: line) + public func security(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .security, category: category, function: function, file: file, line: line) } /// Logs authentication events (login, logout, token refresh). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func auth( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .auth, category: category, fileID: fileID, function: function, file: file, line: line) + public func auth(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .auth, category: category, function: function, file: file, line: line) } // MARK: - Performance & Analytics @@ -311,38 +215,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func metric( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .metric, category: category, fileID: fileID, function: function, file: file, line: line) + public func metric(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .metric, category: category, function: function, file: file, line: line) } /// Logs analytics events (user behavior, feature usage). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func analytics( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .analytics, category: category, fileID: fileID, function: function, file: file, line: line) + public func analytics(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .analytics, category: category, function: function, file: file, line: line) } // MARK: - UI & User Interaction @@ -351,38 +239,29 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func ui( + public func ui( _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, function: String = #function, file: String = #file, line: Int = #line ) { - log(message, level: .ui, category: category, fileID: fileID, function: function, file: file, line: line) + log(message, level: .ui, category: category, function: function, file: file, line: line) } /// Logs user actions (taps, gestures, input). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func user( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .user, category: category, fileID: fileID, function: function, file: file, line: line) + public func user(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .user, category: category, function: function, file: file, line: line) } // MARK: - Database & Storage @@ -391,50 +270,27 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func database( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .database, category: category, fileID: fileID, function: function, file: file, line: line) + public func database(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .database, category: category, function: function, file: file, line: line) } /// Logs storage operations (file I/O, cache, persistence). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func storage( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .storage, category: category, fileID: fileID, function: function, file: file, line: line) + public func storage(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .storage, category: category, function: function, file: file, line: line) } - private static func log( - _ message: String, - level: LogLevel, - category: LogCategory?, - fileID: String, - function: String, - file: String, - line: Int + private func log( + _ message: String, level: LogLevel, category: LogCategory, function: String, file: String, line: Int ) { - let resolvedCategory = category ?? LogCategory(fileID, isAutoGenerated: true) - logger.log(message, level: level, category: resolvedCategory, file: file, function: function, line: line) + Log.logger.log(message, level: level, category: category, file: file, function: function, line: line) } } diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift new file mode 100644 index 0000000..2ecea29 --- /dev/null +++ b/Sources/Scribe/Macros.swift @@ -0,0 +1,110 @@ +// +// Macros.swift +// Scribe +// +// Created by Kai Azim on 2026-01-07. +// + +#if $Macros && hasAttribute(attached) + + /// A style of log to be added via the macro. + public enum LogStyle { + /// Makes the `log` a static property. + case `static` + + /// Makes the `log` an instance property. + case instance + } + + /// Defines and implements a log and a category. + /// + /// This macro helps to implement log helpers to ease the use of Scribe across your project. + /// + /// # Usage + /// + /// The `@Scribable` macro allows a type to automatically provide: + /// - a `Log` instance + /// - a `LogCategory` instance + /// specific to that type. + /// + /// Why is this useful? + /// Normally, you would need to define a `LogCategory` for each type and pass it into every log statement. + /// It's easy to forget to create or use the correct category, which can lead to inconsistent logging. + /// `@Scribable` solves this by automatically generating the appropriate category for you, + /// ensuring that every log statement is correctly categorized. + /// + /// Example before using `@Scribable`: + /// ```swift + /// final class Updater { + /// func fetchLatestInfo() { + /// Log.info("Checking for updates...", category: .updater) + /// // ... + /// Log.info("Finished for updates!", category: .updater) + /// } + /// + /// func installUpdate() { + /// Log.info("Installing update...", category: .updater) + /// // ... + /// Log.info("Installed update, ready to relaunch!", category: .updater) + /// } + /// } + /// + /// extension LogCategory { + /// static let updater = LogCategory("Updater") + /// } + /// ``` + /// + /// With `@Scribable`: + /// ```swift + /// @Scribable + /// final class Updater { + /// func fetchLatestInfo() { + /// log.info("Checking for updates...") + /// // ... + /// log.info("Finished for updates!") + /// } + /// + /// func installUpdate() { + /// log.info("Installing update...") + /// // ... + /// log.info("Installed update, ready to relaunch!") + /// } + /// } + /// ``` + /// + /// Using this macro gives you a cleaner, safer workflow: + /// - No need to manually define a `LogCategory` + /// - You won’t forget to add the correct category + /// - Every log statement automatically uses the right category for the type + /// + /// # Log Styles + /// + /// This macro attaches two or three properties to the given type depending on the used log style: + /// + /// **Static Log Style** + /// + /// Usage: `@Scribable(.static)` + /// + /// - A static property with the name `log` is exposed, of type `Log`. + /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the + /// Log to help categorize this type. + /// + /// ## Instance Log Style + /// + /// Usage: `@Scribable(.instance)` or simply `@Scribable` + /// + /// - An instance property with the name `log` is exposed, of type `Log`. This is actually just a computed variable + /// for the static property defined below: + /// - A private, static property with the name `_log` is exposed, of type `Log`. Expect this to work in the same way + /// that the static log style defines `log`, just that it is private. This is used to store a single, shared + /// instance across all instances of this type. + /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the + /// Log to help categorize this type. + /// + @attached(member, names: named(log), named(_log), named(_logCategory)) + public macro Scribable(_ style: LogStyle = .instance) = #externalMacro( + module: "ScribeMacros", + type: "ScribableMacro" + ) + +#endif diff --git a/Sources/ScribeMacros/ScribableMacro.swift b/Sources/ScribeMacros/ScribableMacro.swift new file mode 100644 index 0000000..44e9a98 --- /dev/null +++ b/Sources/ScribeMacros/ScribableMacro.swift @@ -0,0 +1,129 @@ +// +// ScribableMacro.swift +// Scribe +// +// Created by Kai Azim on 2026-01-07. +// + +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +// MARK: - MacrosPlugin + +@main +struct MacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + ScribableMacro.self + ] +} + +// MARK: - ScribableMacro + +struct ScribableMacro: MemberMacro { + /// A style of log to be added via the macro. Make sure this matches the declarations over in the Scribe target! + enum LogStyle { + /// Makes the `log` a static property. + case `static` + + /// Makes the `log` an instance property. + case instance + } + + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclSyntaxProtocol, + conformingTo protocolType: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let name: String + + if let decl = declaration.as(EnumDeclSyntax.self) { + name = decl.name.text + } else if let decl = declaration.as(StructDeclSyntax.self) { + name = decl.name.text + } else if let decl = declaration.as(ClassDeclSyntax.self) { + name = decl.name.text + } else { + return [] + } + + let style = parseLogStyle(from: node) + + var members: [DeclSyntax] = [ + DeclSyntax( + """ + public static let _logCategory: LogCategory = LogCategory("\(raw: name)") + """ + ) + ] + + switch style { + case .static: + members.append( + DeclSyntax( + """ + public static let log = Log(category: _logCategory) + """ + ) + ) + case .instance: + members.append( + DeclSyntax( + """ + private static let _log = Log(category: Self._logCategory) + """ + ) + ) + + members.append( + DeclSyntax( + """ + public var log: Log { Self._log } + """ + ) + ) + } + + return members + } + + /// Parses the logger style to be used with this macro. + /// + /// By default, the `.instance` style is used, and the `.static` style is only used if explicitly set. + /// + /// Examples: + /// - `@Scribable` -> instance style + /// - `@Scribable(.static)` -> static style + /// - `@Scribable(.instance)` -> instance style + /// + /// - Parameter node: The node associated with this macro. The first argument will be used to determine the style. + /// - Returns: A LoggerStyle case depending on the node. + private static func parseLogStyle(from node: AttributeSyntax) -> LogStyle { + // When nothing is provided, assume .instance + guard let tuple = node.arguments?.as(LabeledExprListSyntax.self), + let first = tuple.first + else { + return .instance + } + + let expr = first.expression + + // Only accept MemberAccessExprSyntax (.static or .instance) + guard let memberAccess = expr.as(MemberAccessExprSyntax.self) else { + return .instance + } + + let identifier = memberAccess.declName.baseName.text + + switch identifier { + case "static": + return .static + case "instance": + return .instance + default: + return .instance + } + } +} From 276df84d60a0c83aee4c3c0032edce6b03007c70 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Thu, 8 Jan 2026 11:37:40 -0700 Subject: [PATCH 2/6] Loggable macro --- Sources/Scribe/Log.swift | 4 +- Sources/Scribe/Macros.swift | 26 ++-- Sources/ScribeMacros/LoggableMacro.swift | 146 ++++++++++++++++++++++ Sources/ScribeMacros/ScribableMacro.swift | 129 ------------------- 4 files changed, 165 insertions(+), 140 deletions(-) create mode 100644 Sources/ScribeMacros/LoggableMacro.swift delete mode 100644 Sources/ScribeMacros/ScribableMacro.swift diff --git a/Sources/Scribe/Log.swift b/Sources/Scribe/Log.swift index 5b1fcff..8e4e631 100644 --- a/Sources/Scribe/Log.swift +++ b/Sources/Scribe/Log.swift @@ -19,11 +19,11 @@ /// log.debug("New user registered") /// ``` /// -/// It is recommended to use this alongside the `@Scribable` macro, which automatically creates the `Log` for you. +/// It is recommended to use this alongside the `@Loggable` macro, which automatically creates the `Log` for you. /// You can use is as shown below: /// /// ```swift -/// @Scribable +/// @Loggable /// struct TokenManager { /// func test() { /// log.success("New token generated") diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift index 2ecea29..c8924db 100644 --- a/Sources/Scribe/Macros.swift +++ b/Sources/Scribe/Macros.swift @@ -22,7 +22,7 @@ /// /// # Usage /// - /// The `@Scribable` macro allows a type to automatically provide: + /// The `@Loggable` macro allows a type to automatically provide: /// - a `Log` instance /// - a `LogCategory` instance /// specific to that type. @@ -30,10 +30,10 @@ /// Why is this useful? /// Normally, you would need to define a `LogCategory` for each type and pass it into every log statement. /// It's easy to forget to create or use the correct category, which can lead to inconsistent logging. - /// `@Scribable` solves this by automatically generating the appropriate category for you, + /// `@Loggable` solves this by automatically generating the appropriate category for you, /// ensuring that every log statement is correctly categorized. /// - /// Example before using `@Scribable`: + /// Example before using `@Loggable`: /// ```swift /// final class Updater { /// func fetchLatestInfo() { @@ -54,9 +54,9 @@ /// } /// ``` /// - /// With `@Scribable`: + /// With `@Loggable`: /// ```swift - /// @Scribable + /// @Loggable /// final class Updater { /// func fetchLatestInfo() { /// log.info("Checking for updates...") @@ -83,7 +83,7 @@ /// /// **Static Log Style** /// - /// Usage: `@Scribable(.static)` + /// Usage: `@Loggable(.static)` /// /// - A static property with the name `log` is exposed, of type `Log`. /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the @@ -91,7 +91,7 @@ /// /// ## Instance Log Style /// - /// Usage: `@Scribable(.instance)` or simply `@Scribable` + /// Usage: `@Loggable(.instance)` or simply `@Loggable` /// /// - An instance property with the name `log` is exposed, of type `Log`. This is actually just a computed variable /// for the static property defined below: @@ -101,10 +101,18 @@ /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the /// Log to help categorize this type. /// + /// # Naming + /// + /// By default, the `@Loggable` macro uses the type’s name as the log category. + /// If you’d like to override this, you can provide a custom name as the macro’s first argument. + /// + /// For example, applying `@Loggable("Network")` to a type named `NetworkManager` + /// will create a `LogCategory` named `Network` instead of `NetworkManager`. + /// @attached(member, names: named(log), named(_log), named(_logCategory)) - public macro Scribable(_ style: LogStyle = .instance) = #externalMacro( + public macro Loggable(_ name: StaticString? = nil, _ style: LogStyle = .instance) = #externalMacro( module: "ScribeMacros", - type: "ScribableMacro" + type: "LoggableMacro" ) #endif diff --git a/Sources/ScribeMacros/LoggableMacro.swift b/Sources/ScribeMacros/LoggableMacro.swift new file mode 100644 index 0000000..956f051 --- /dev/null +++ b/Sources/ScribeMacros/LoggableMacro.swift @@ -0,0 +1,146 @@ +// +// LoggableMacro.swift +// Scribe +// +// Created by Kai Azim on 2026-01-07. +// + +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +// MARK: - MacrosPlugin + +@main +struct MacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + LoggableMacro.self + ] +} + +// MARK: - LoggableMacro + +struct LoggableMacro: MemberMacro { + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclSyntaxProtocol, + conformingTo protocolType: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + var options = parseLogOptions(from: node) + + if options.name == nil { + if let decl = declaration.as(EnumDeclSyntax.self) { + options.name = decl.name.text + } else if let decl = declaration.as(StructDeclSyntax.self) { + options.name = decl.name.text + } else if let decl = declaration.as(ClassDeclSyntax.self) { + options.name = decl.name.text + } else { + return [] + } + } + + var members: [DeclSyntax] = [ + DeclSyntax( + """ + public static let _logCategory: LogCategory = LogCategory("\(raw: options.name)") + """ + ) + ] + + switch options.style { + case .static: + members.append( + DeclSyntax( + """ + public static let log = Log(category: _logCategory) + """ + ) + ) + case .instance: + members.append( + DeclSyntax( + """ + private static let _log = Log(category: Self._logCategory) + """ + ) + ) + + members.append( + DeclSyntax( + """ + public var log: Log { Self._log } + """ + ) + ) + } + + return members + } + + /// Parses the logger name and style to be used with this macro. + /// + /// By default: + /// - `name` is `nil` + /// - `style` is `.instance` + /// + /// Supported forms: + /// - `@Loggable` + /// - `@Loggable("network")` + /// - `@Loggable(.static)` + /// - `@Loggable("network", .static)` + private static func parseLogOptions(from node: AttributeSyntax) -> ParsedLogOptions { + var name: String? = nil + var style: LogStyle = .instance + + guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) else { + return ParsedLogOptions(name: name, style: style) + } + + for argument in arguments { + let expr = argument.expression + + // String literal → name + if let stringLiteral = expr.as(StringLiteralExprSyntax.self) { + name = stringLiteral.segments + .compactMap { $0.as(StringSegmentSyntax.self)?.content.text } + .joined() + continue + } + + // Member access → style (.static / .instance) + if let memberAccess = expr.as(MemberAccessExprSyntax.self) { + switch memberAccess.declName.baseName.text { + case "static": + style = .static + case "instance": + style = .instance + default: + break + } + } + } + + return ParsedLogOptions(name: name, style: style) + } + + /// A style of log to be added via the macro. Make sure this matches the declarations over in the Scribe target! + private enum LogStyle { + /// Makes the `log` a static property. + case `static` + + /// Makes the `log` an instance property. + case instance + } + + /// A struct to expose a type-safe version of the macro's arguments. + private struct ParsedLogOptions { + /// The name of the LogCategory. If not present, the type's name is used. + var name: String? + + /// The style of log to be added via the macro. + let style: LogStyle + } +} diff --git a/Sources/ScribeMacros/ScribableMacro.swift b/Sources/ScribeMacros/ScribableMacro.swift deleted file mode 100644 index 44e9a98..0000000 --- a/Sources/ScribeMacros/ScribableMacro.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// ScribableMacro.swift -// Scribe -// -// Created by Kai Azim on 2026-01-07. -// - -import SwiftCompilerPlugin -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -// MARK: - MacrosPlugin - -@main -struct MacrosPlugin: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - ScribableMacro.self - ] -} - -// MARK: - ScribableMacro - -struct ScribableMacro: MemberMacro { - /// A style of log to be added via the macro. Make sure this matches the declarations over in the Scribe target! - enum LogStyle { - /// Makes the `log` a static property. - case `static` - - /// Makes the `log` an instance property. - case instance - } - - static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclSyntaxProtocol, - conformingTo protocolType: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - let name: String - - if let decl = declaration.as(EnumDeclSyntax.self) { - name = decl.name.text - } else if let decl = declaration.as(StructDeclSyntax.self) { - name = decl.name.text - } else if let decl = declaration.as(ClassDeclSyntax.self) { - name = decl.name.text - } else { - return [] - } - - let style = parseLogStyle(from: node) - - var members: [DeclSyntax] = [ - DeclSyntax( - """ - public static let _logCategory: LogCategory = LogCategory("\(raw: name)") - """ - ) - ] - - switch style { - case .static: - members.append( - DeclSyntax( - """ - public static let log = Log(category: _logCategory) - """ - ) - ) - case .instance: - members.append( - DeclSyntax( - """ - private static let _log = Log(category: Self._logCategory) - """ - ) - ) - - members.append( - DeclSyntax( - """ - public var log: Log { Self._log } - """ - ) - ) - } - - return members - } - - /// Parses the logger style to be used with this macro. - /// - /// By default, the `.instance` style is used, and the `.static` style is only used if explicitly set. - /// - /// Examples: - /// - `@Scribable` -> instance style - /// - `@Scribable(.static)` -> static style - /// - `@Scribable(.instance)` -> instance style - /// - /// - Parameter node: The node associated with this macro. The first argument will be used to determine the style. - /// - Returns: A LoggerStyle case depending on the node. - private static func parseLogStyle(from node: AttributeSyntax) -> LogStyle { - // When nothing is provided, assume .instance - guard let tuple = node.arguments?.as(LabeledExprListSyntax.self), - let first = tuple.first - else { - return .instance - } - - let expr = first.expression - - // Only accept MemberAccessExprSyntax (.static or .instance) - guard let memberAccess = expr.as(MemberAccessExprSyntax.self) else { - return .instance - } - - let identifier = memberAccess.declName.baseName.text - - switch identifier { - case "static": - return .static - case "instance": - return .instance - default: - return .instance - } - } -} From 586df8de9ee651c350d39e1f0589e17198052aa3 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Thu, 8 Jan 2026 16:56:57 -0700 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9E=20return=20error=20if=20Loggab?= =?UTF-8?q?le=20gets=20a=20category=20name=20and=20a=20LogCategory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Scribe/Macros.swift | 7 +- Sources/ScribeMacros/LoggableMacro.swift | 82 +++++++++++++++++++++--- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift index c8924db..8ca76ba 100644 --- a/Sources/Scribe/Macros.swift +++ b/Sources/Scribe/Macros.swift @@ -110,9 +110,12 @@ /// will create a `LogCategory` named `Network` instead of `NetworkManager`. /// @attached(member, names: named(log), named(_log), named(_logCategory)) - public macro Loggable(_ name: StaticString? = nil, _ style: LogStyle = .instance) = #externalMacro( + public macro Loggable( + _ name: StaticString? = nil, + category: LogCategory? = nil, + style: LogStyle = .instance + ) = #externalMacro( module: "ScribeMacros", type: "LoggableMacro" ) - #endif diff --git a/Sources/ScribeMacros/LoggableMacro.swift b/Sources/ScribeMacros/LoggableMacro.swift index 956f051..ae51090 100644 --- a/Sources/ScribeMacros/LoggableMacro.swift +++ b/Sources/ScribeMacros/LoggableMacro.swift @@ -6,6 +6,7 @@ // import SwiftCompilerPlugin +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -30,7 +31,18 @@ struct LoggableMacro: MemberMacro { ) throws -> [DeclSyntax] { var options = parseLogOptions(from: node) - if options.name == nil { + if options.name != nil, options.categoryExpr != nil { + context.diagnose( + Diagnostic( + node: Syntax(node), + message: LoggableArgumentConflictDiagnostic() + ) + ) + return [] + } + + // If no explicit category is provided, fall back to type name + if options.categoryExpr == nil, options.name == nil { if let decl = declaration.as(EnumDeclSyntax.self) { options.name = decl.name.text } else if let decl = declaration.as(StructDeclSyntax.self) { @@ -42,13 +54,29 @@ struct LoggableMacro: MemberMacro { } } - var members: [DeclSyntax] = [ - DeclSyntax( - """ - public static let _logCategory: LogCategory = LogCategory("\(raw: options.name)") - """ + var members: [DeclSyntax] = [] + + if let categoryExpr = options.categoryExpr { + // If the user provided a category, then make computed static var + members.append( + DeclSyntax( + """ + public static var _logCategory: LogCategory { + \(categoryExpr) + } + """ + ) ) - ] + } else if let name = options.name { + // Otherwise, create a new category + members.append( + DeclSyntax( + """ + public static let _logCategory: LogCategory = LogCategory("\(raw: name)") + """ + ) + ) + } switch options.style { case .static: @@ -83,6 +111,7 @@ struct LoggableMacro: MemberMacro { /// Parses the logger name and style to be used with this macro. /// /// By default: + /// - `categoryExpr` is `nil` /// - `name` is `nil` /// - `style` is `.instance` /// @@ -90,18 +119,27 @@ struct LoggableMacro: MemberMacro { /// - `@Loggable` /// - `@Loggable("network")` /// - `@Loggable(.static)` + /// - `@Loggable(category: .network)` /// - `@Loggable("network", .static)` + /// - `@Loggable(category: .network, .instance)` private static func parseLogOptions(from node: AttributeSyntax) -> ParsedLogOptions { var name: String? = nil + var categoryExpr: ExprSyntax? = nil var style: LogStyle = .instance guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) else { - return ParsedLogOptions(name: name, style: style) + return ParsedLogOptions(categoryExpr: nil, name: nil, style: style) } for argument in arguments { let expr = argument.expression + // category: + if argument.label?.text == "category" { + categoryExpr = expr + continue + } + // String literal → name if let stringLiteral = expr.as(StringLiteralExprSyntax.self) { name = stringLiteral.segments @@ -123,7 +161,11 @@ struct LoggableMacro: MemberMacro { } } - return ParsedLogOptions(name: name, style: style) + return ParsedLogOptions( + categoryExpr: categoryExpr, + name: name, + style: style + ) } /// A style of log to be added via the macro. Make sure this matches the declarations over in the Scribe target! @@ -137,10 +179,30 @@ struct LoggableMacro: MemberMacro { /// A struct to expose a type-safe version of the macro's arguments. private struct ParsedLogOptions { - /// The name of the LogCategory. If not present, the type's name is used. + /// Explicit category expression, e.g. `.network`. + var categoryExpr: ExprSyntax? + + /// The name of the LogCategory to generate. If the `name` and `categoryExpr` are `nil`, then the type name is + /// used. var name: String? /// The style of log to be added via the macro. let style: LogStyle } } + +// MARK: - LoggableArgumentConflictDiagnostic + +private struct LoggableArgumentConflictDiagnostic: DiagnosticMessage { + var message: String { + "`@Loggable` cannot specify both a custom name and a category. Use only one." + } + + var diagnosticID: MessageID { + MessageID(domain: "LoggableMacro", id: "nameAndCategoryConflict") + } + + var severity: DiagnosticSeverity { + .error + } +} From aea0a10e6da8f12a64b357eb653acbc2aa4ec4d7 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sat, 17 Jan 2026 02:51:56 -0700 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20the=20Logga?= =?UTF-8?q?ble=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Scribe/Macros.swift | 4 +- Sources/ScribeMacros/LoggableMacro.swift | 88 +++++++++++++++---- Tests/Scribe/LogCategory+Extensions.swift | 21 +++++ Tests/Scribe/LoggableTests.swift | 100 ++++++++++++++++++++++ Tests/Scribe/ScribeTests.swift | 14 --- 5 files changed, 194 insertions(+), 33 deletions(-) create mode 100644 Tests/Scribe/LogCategory+Extensions.swift create mode 100644 Tests/Scribe/LoggableTests.swift diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift index 8ca76ba..2c1d6f2 100644 --- a/Sources/Scribe/Macros.swift +++ b/Sources/Scribe/Macros.swift @@ -83,7 +83,7 @@ /// /// **Static Log Style** /// - /// Usage: `@Loggable(.static)` + /// Usage: `@Loggable(style: .static)` /// /// - A static property with the name `log` is exposed, of type `Log`. /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the @@ -91,7 +91,7 @@ /// /// ## Instance Log Style /// - /// Usage: `@Loggable(.instance)` or simply `@Loggable` + /// Usage: `@Loggable(style: .instance)` or simply `@Loggable` /// /// - An instance property with the name `log` is exposed, of type `Log`. This is actually just a computed variable /// for the static property defined below: diff --git a/Sources/ScribeMacros/LoggableMacro.swift b/Sources/ScribeMacros/LoggableMacro.swift index ae51090..0bf16db 100644 --- a/Sources/ScribeMacros/LoggableMacro.swift +++ b/Sources/ScribeMacros/LoggableMacro.swift @@ -29,8 +29,15 @@ struct LoggableMacro: MemberMacro { conformingTo protocolType: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { + guard let typeName = getTypeName(declaration: declaration) else { + return [] + } + + let accessLevel = getAccessLevel(declaration: declaration) + let accessPrefix = accessLevel.isEmpty ? "" : "\(accessLevel) " var options = parseLogOptions(from: node) + // If both a custom name and category are passed in, they conflict. if options.name != nil, options.categoryExpr != nil { context.diagnose( Diagnostic( @@ -43,15 +50,7 @@ struct LoggableMacro: MemberMacro { // If no explicit category is provided, fall back to type name if options.categoryExpr == nil, options.name == nil { - if let decl = declaration.as(EnumDeclSyntax.self) { - options.name = decl.name.text - } else if let decl = declaration.as(StructDeclSyntax.self) { - options.name = decl.name.text - } else if let decl = declaration.as(ClassDeclSyntax.self) { - options.name = decl.name.text - } else { - return [] - } + options.name = typeName } var members: [DeclSyntax] = [] @@ -61,7 +60,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - public static var _logCategory: LogCategory { + \(raw: accessPrefix)static var _logCategory: LogCategory { \(categoryExpr) } """ @@ -72,7 +71,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - public static let _logCategory: LogCategory = LogCategory("\(raw: name)") + \(raw: accessPrefix)static let _logCategory: LogCategory = LogCategory("\(raw: name)") """ ) ) @@ -83,7 +82,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - public static let log = Log(category: _logCategory) + \(raw: accessPrefix)static let log = Log(category: _logCategory) """ ) ) @@ -91,7 +90,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - private static let _log = Log(category: Self._logCategory) + private static let _log = Log(category: \(raw: typeName)._logCategory) """ ) ) @@ -99,7 +98,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - public var log: Log { Self._log } + \(raw: accessPrefix)var log: Log { \(raw: typeName)._log } """ ) ) @@ -108,6 +107,61 @@ struct LoggableMacro: MemberMacro { return members } + /// Extracts the name of the type from the declaration. + /// + /// This is used to: + /// - Generate the default `LogCategory` name when no custom name is provided. + /// - Reference the type explicitly in the generated `_log` property (e.g., `TypeName._logCategory`). + /// This is because using `Self.<>` can lead to covariant self errors. + /// + /// Returns `nil` if the declaration is not a supported type (enum, struct, or class). + private static func getTypeName(declaration: some DeclSyntaxProtocol) -> String? { + if let decl = declaration.as(EnumDeclSyntax.self) { + decl.name.text + } else if let decl = declaration.as(StructDeclSyntax.self) { + decl.name.text + } else if let decl = declaration.as(ClassDeclSyntax.self) { + decl.name.text + } else { + nil + } + } + + /// Extracts the access level modifier from the declaration. + /// + /// This ensures the generated properties (`_logCategory`, `log`) match the access level of the type they're + /// attached to, rather than being hardcoded to `public`. + /// + /// Returns an empty string if no explicit access level is found. + private static func getAccessLevel(declaration: some DeclSyntaxProtocol) -> String { + let modifiers: DeclModifierListSyntax? = if let decl = declaration.as(EnumDeclSyntax.self) { + decl.modifiers + } else if let decl = declaration.as(StructDeclSyntax.self) { + decl.modifiers + } else if let decl = declaration.as(ClassDeclSyntax.self) { + decl.modifiers + } else { + nil + } + + guard let modifiers else { return "" } + + for modifier in modifiers { + switch modifier.name.text { + case "public", + "package", + "internal", + "fileprivate", + "private": + return modifier.name.text + default: + continue + } + } + + return "" + } + /// Parses the logger name and style to be used with this macro. /// /// By default: @@ -118,10 +172,10 @@ struct LoggableMacro: MemberMacro { /// Supported forms: /// - `@Loggable` /// - `@Loggable("network")` - /// - `@Loggable(.static)` + /// - `@Loggable(type: .static)` /// - `@Loggable(category: .network)` - /// - `@Loggable("network", .static)` - /// - `@Loggable(category: .network, .instance)` + /// - `@Loggable("network", type: .static)` + /// - `@Loggable(category: .network, type: .instance)` private static func parseLogOptions(from node: AttributeSyntax) -> ParsedLogOptions { var name: String? = nil var categoryExpr: ExprSyntax? = nil diff --git a/Tests/Scribe/LogCategory+Extensions.swift b/Tests/Scribe/LogCategory+Extensions.swift new file mode 100644 index 0000000..08bd55a --- /dev/null +++ b/Tests/Scribe/LogCategory+Extensions.swift @@ -0,0 +1,21 @@ +// +// LogCategory+Extensions.swift +// Scribe +// +// Created by Kai Azim on 2026-01-17. +// + +import Scribe + +extension LogCategory { + static let test = LogCategory("TestCategory") + + static let testAllowed = LogCategory("AllowedCategory") + static let testBlocked = LogCategory("BlockedCategory") + + static let evictOneAuto = LogCategory("Auto/EvictOne.swift", isAutoGenerated: true) + static let evictTwoAuto = LogCategory("Auto/EvictTwo.swift", isAutoGenerated: true) + static let evictThreeAuto = LogCategory("Auto/EvictThree.swift", isAutoGenerated: true) + + static let passedInCategory = LogCategory("PassedInCategory") +} diff --git a/Tests/Scribe/LoggableTests.swift b/Tests/Scribe/LoggableTests.swift new file mode 100644 index 0000000..8940150 --- /dev/null +++ b/Tests/Scribe/LoggableTests.swift @@ -0,0 +1,100 @@ +import XCTest +@testable import Scribe + +// MARK: - LoggableClass + +@Loggable +final class LoggableClass { + func doWork() { log.info("LoggableClass working") } +} + +// MARK: - LoggableStruct + +@Loggable +struct LoggableStruct { + func doWork() { log.info("LoggableStruct working") } +} + +// MARK: - LoggableEnum + +@Loggable +enum LoggableEnum { + case one + func doWork() { log.info("LoggableEnum working") } +} + +// MARK: - LoggableCustomName + +@Loggable("CustomName") +final class LoggableCustomName { + func doWork() { log.info("LoggableCustomName working") } +} + +// MARK: - LoggablePassedCategory + +@Loggable(category: .passedInCategory) +final class LoggablePassedCategory { + func doWork() { log.info("LoggablePassedCategory working") } +} + +// MARK: - LoggableClassStatic + +@Loggable(style: .static) +final class LoggableClassStatic { + static func doWork() { log.info("LoggableClassStatic working") } +} + +// MARK: - LoggableMacroTests + +final class LoggableMacroTests: XCTestCase { + override func setUp() { + super.setUp() + Log.logger.setMinimumLevel(.debug) + Log.logger.setConfiguration(.default) + Log.logger.removeAllSinks() + } + + func testLoggableClass() { + XCTAssertEqual(LoggableClass._logCategory.name, "LoggableClass") + XCTAssertEqual(LoggableClass().log.category.name, "LoggableClass") + } + + func testLoggableStruct() { + XCTAssertEqual(LoggableStruct._logCategory.name, "LoggableStruct") + XCTAssertEqual(LoggableStruct().log.category.name, "LoggableStruct") + } + + func testLoggableEnum() { + XCTAssertEqual(LoggableEnum._logCategory.name, "LoggableEnum") + XCTAssertEqual(LoggableEnum.one.log.category.name, "LoggableEnum") + } + + func testLoggableStaticStyle() { + XCTAssertEqual(LoggableClassStatic._logCategory.name, "LoggableClassStatic") + XCTAssertEqual(LoggableClassStatic.log.category.name, "LoggableClassStatic") + } + + func testLoggableCustomName() { + XCTAssertEqual(LoggableCustomName._logCategory.name, "CustomName") + XCTAssertEqual(LoggableCustomName().log.category.name, "CustomName") + } + + func testLoggablePassedCategory() { + XCTAssertEqual(LoggablePassedCategory._logCategory.name, "PassedInCategory") + XCTAssertEqual(LoggablePassedCategory().log.category.name, "PassedInCategory") + } + + func testLoggableActuallyLogs() { + let expectation = XCTestExpectation(description: "Log received") + + Log.logger.addSink { message in + if message.contains("LoggableClass working"), message.contains("[LoggableClass]") { + expectation.fulfill() + } + } + + LoggableClass().doWork() + + wait(for: [expectation], timeout: 2.0) + } +} diff --git a/Tests/Scribe/ScribeTests.swift b/Tests/Scribe/ScribeTests.swift index 51f7a28..efdf5a1 100644 --- a/Tests/Scribe/ScribeTests.swift +++ b/Tests/Scribe/ScribeTests.swift @@ -1,9 +1,6 @@ import XCTest - @testable @_spi(Internals) import Scribe -// MARK: - ScribeTests - final class ScribeTests: XCTestCase { override func setUp() { super.setUp() @@ -363,14 +360,3 @@ final class ScribeTests: XCTestCase { Log.logger.removeSink(sinkID) } } - -extension LogCategory { - static let test = LogCategory("TestCategory") - - static let testAllowed = LogCategory("AllowedCategory") - static let testBlocked = LogCategory("BlockedCategory") - - static let evictOneAuto = LogCategory("Auto/EvictOne.swift", isAutoGenerated: true) - static let evictTwoAuto = LogCategory("Auto/EvictTwo.swift", isAutoGenerated: true) - static let evictThreeAuto = LogCategory("Auto/EvictThree.swift", isAutoGenerated: true) -} From 0f4b175364cfe850fb475f88d0506dce87e50d9b Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sat, 17 Jan 2026 02:59:09 -0700 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=93=84=20Update=20README=20for=20Logg?= =?UTF-8?q?able=20and=20missing=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d322a50..886e231 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,25 @@ extension LogCategory { - Each category maps to its own `Logger` instance under the same subsystem, so Console and Xcode show first-class category filters without extra configuration. +### @Loggable Macro + +Automatically generates a `log` property for classes, structs, and enums. + +```swift +@Loggable +struct TokenManager { + func refresh() { + log.info("Refreshing token") + } +} +``` + +**Options:** +- `@Loggable` — uses the type name as the category +- `@Loggable("CustomName")` - uses a custom category name +- `@Loggable(category: .network)` - uses an existing `LogCategory` +- `@Loggable(style: .static)` - generates a static `log` property instead of instance + ### LogManager Core logger with configuration and sinks. @@ -112,13 +131,18 @@ Core logger with configuration and sinks. - `minimumLevel: LogLevel` — threshold (read/write) - `configuration: LogConfiguration` — formatting and filtering (read/write) - `sinkCount: Int` — number of registered sinks + - `loggerCacheCount: Int` — number of cached `Logger` instances - **Methods**: - `log(_:level:category:file:function:line:)` — core logging method - `setMinimumLevel(_:)` — async level setter + - `getMinimumLevel(_:)` — async level getter with completion handler - `setConfiguration(_:)` — async configuration setter - - `addSink(_:) -> SinkID` — register a sink, returns ID for removal + - `getConfiguration(_:)` — async configuration getter with completion handler + - `addSink(categories:_:) -> LogSubscription` — register a sink with optional category filter, returns ID for removal - `removeSink(_:)` — remove a specific sink by ID - `removeAllSinks()` — remove all sinks + - `stream(categories:) -> AsyncStream` — create an async stream of log messages with optional category filter + - `clearLoggerCache(completion:)` — clear cached `Logger` instances ### LogConfiguration @@ -129,6 +153,7 @@ Configuration struct for customizing log output. - `includeTimestamp: Bool` — include timestamps (default: `true`) - `includeEmoji: Bool` — include level emoji (default: `true`) - `includeShortCode: Bool` — include level short code like `[DBG]` (default: `false`) +- `includeFileAndLineNumber: Bool` — include source file and line number (default: `true`) - `autoLoggerCacheLimit: Int?` — limit cached auto-generated `Logger` instances (e.g., `#fileID`); `nil` means unbounded; default is 100 - `dateFormat: String` — timestamp format (default: `"yyyy-MM-dd HH:mm:ss.SSSZ"`) @@ -275,8 +300,8 @@ LogManager.shared.clearLoggerCache() ``` - Notes: - - Auto-generated categories (`#fileID`) are cached in an internal `NSCache` with a default cap of 100. - - Custom categories you define (e.g., `LogCategory("APIService")`) are stored in a dictionary and are not evicted. + - Auto-generated categories (`#fileID`) are cached with a default limit of 100. When the limit is reached, the least recently used loggers are removed first. + - Custom categories you define (e.g., `LogCategory("APIService")`) are cached permanently and not removed. ### Combined Configuration From 108dca7e5c9e1eb99b327b39d384cf76c1f22223 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sat, 17 Jan 2026 03:15:18 -0700 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=9A=9B=20=5FlogCategory=20->=20logCat?= =?UTF-8?q?egory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +++++++++++++++++------ Sources/Scribe/Macros.swift | 6 ++--- Sources/ScribeMacros/LoggableMacro.swift | 12 +++++----- Tests/Scribe/LogCategory+Extensions.swift | 9 +------ Tests/Scribe/LoggableTests.swift | 12 +++++----- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 886e231..0b330c6 100644 --- a/README.md +++ b/README.md @@ -118,9 +118,9 @@ struct TokenManager { **Options:** - `@Loggable` — uses the type name as the category -- `@Loggable("CustomName")` - uses a custom category name -- `@Loggable(category: .network)` - uses an existing `LogCategory` -- `@Loggable(style: .static)` - generates a static `log` property instead of instance +- `@Loggable("CustomName")` — uses a custom category name +- `@Loggable(category: .network)` — uses an existing `LogCategory` +- `@Loggable(style: .static)` — generates a static `log` property instead of instance ### LogManager @@ -354,6 +354,20 @@ LogManager.shared.removeAllSinks() let count = LogManager.shared.sinkCount ``` +### Streaming + +For async/await, use `stream()` instead of callbacks: + +```swift +let stream = LogManager.shared.stream() + +Task { + for await line in stream { + print("Log:", line) + } +} +``` + ## Threading and Performance - Logging occurs on a dedicated utility queue to minimize call-site blocking. @@ -364,6 +378,7 @@ let count = LogManager.shared.sinkCount ## Best Practices +- Use the `@Loggable` macro to automatically generate a `log` property for your types. - Use categories to group logs by module or feature (e.g., `LogCategory("APIService")`, `LogCategory("Storage")`). - Raise `minimumLevel` in production (e.g., `.info` or `.warning`). - Avoid logging PII or secrets; this package does not perform encryption or redaction. @@ -375,14 +390,14 @@ let count = LogManager.shared.sinkCount ```swift extension LogCategory { static let app = LogCategory("App") - static let apiService = LogCategory("APIService") } +@Loggable final class APIService { func fetchProfile() { - Log.api("GET /v1/profile", category: .apiService) + log.api("GET /v1/profile") // ... network call ... - Log.debug("Decoded Profile(id: 123)", category: .apiService) + log.debug("Decoded Profile(id: 123)") } } @@ -393,7 +408,7 @@ struct MyApp: App { LogManager.shared.minimumLevel = .info let config = LogConfiguration( - enabledCategories: [.app, .apiService], + enabledCategories: [.app, APIService.logCategory], includeTimestamp: true ) LogManager.shared.configuration = config diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift index 2c1d6f2..4ca3f7c 100644 --- a/Sources/Scribe/Macros.swift +++ b/Sources/Scribe/Macros.swift @@ -86,7 +86,7 @@ /// Usage: `@Loggable(style: .static)` /// /// - A static property with the name `log` is exposed, of type `Log`. - /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the + /// - A static property with the name `logCategory` is exposed, of type `LogCategory`. This category is used in the /// Log to help categorize this type. /// /// ## Instance Log Style @@ -98,7 +98,7 @@ /// - A private, static property with the name `_log` is exposed, of type `Log`. Expect this to work in the same way /// that the static log style defines `log`, just that it is private. This is used to store a single, shared /// instance across all instances of this type. - /// - A static property with the name `_logCategory` is exposed, of type `LogCategory`. This category is used in the + /// - A static property with the name `logCategory` is exposed, of type `LogCategory`. This category is used in the /// Log to help categorize this type. /// /// # Naming @@ -109,7 +109,7 @@ /// For example, applying `@Loggable("Network")` to a type named `NetworkManager` /// will create a `LogCategory` named `Network` instead of `NetworkManager`. /// - @attached(member, names: named(log), named(_log), named(_logCategory)) + @attached(member, names: named(log), named(_log), named(logCategory)) public macro Loggable( _ name: StaticString? = nil, category: LogCategory? = nil, diff --git a/Sources/ScribeMacros/LoggableMacro.swift b/Sources/ScribeMacros/LoggableMacro.swift index 0bf16db..69286fd 100644 --- a/Sources/ScribeMacros/LoggableMacro.swift +++ b/Sources/ScribeMacros/LoggableMacro.swift @@ -60,7 +60,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - \(raw: accessPrefix)static var _logCategory: LogCategory { + \(raw: accessPrefix)static var logCategory: LogCategory { \(categoryExpr) } """ @@ -71,7 +71,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - \(raw: accessPrefix)static let _logCategory: LogCategory = LogCategory("\(raw: name)") + \(raw: accessPrefix)static let logCategory: LogCategory = LogCategory("\(raw: name)") """ ) ) @@ -82,7 +82,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - \(raw: accessPrefix)static let log = Log(category: _logCategory) + \(raw: accessPrefix)static let log = Log(category: logCategory) """ ) ) @@ -90,7 +90,7 @@ struct LoggableMacro: MemberMacro { members.append( DeclSyntax( """ - private static let _log = Log(category: \(raw: typeName)._logCategory) + private static let _log = Log(category: \(raw: typeName).logCategory) """ ) ) @@ -111,7 +111,7 @@ struct LoggableMacro: MemberMacro { /// /// This is used to: /// - Generate the default `LogCategory` name when no custom name is provided. - /// - Reference the type explicitly in the generated `_log` property (e.g., `TypeName._logCategory`). + /// - Reference the type explicitly in the generated `_log` property (e.g., `TypeName.logCategory`). /// This is because using `Self.<>` can lead to covariant self errors. /// /// Returns `nil` if the declaration is not a supported type (enum, struct, or class). @@ -129,7 +129,7 @@ struct LoggableMacro: MemberMacro { /// Extracts the access level modifier from the declaration. /// - /// This ensures the generated properties (`_logCategory`, `log`) match the access level of the type they're + /// This ensures the generated properties (`logCategory`, `log`) match the access level of the type they're /// attached to, rather than being hardcoded to `public`. /// /// Returns an empty string if no explicit access level is found. diff --git a/Tests/Scribe/LogCategory+Extensions.swift b/Tests/Scribe/LogCategory+Extensions.swift index 08bd55a..a8a2760 100644 --- a/Tests/Scribe/LogCategory+Extensions.swift +++ b/Tests/Scribe/LogCategory+Extensions.swift @@ -1,11 +1,4 @@ -// -// LogCategory+Extensions.swift -// Scribe -// -// Created by Kai Azim on 2026-01-17. -// - -import Scribe +@testable @_spi(Internals) import Scribe extension LogCategory { static let test = LogCategory("TestCategory") diff --git a/Tests/Scribe/LoggableTests.swift b/Tests/Scribe/LoggableTests.swift index 8940150..540acd2 100644 --- a/Tests/Scribe/LoggableTests.swift +++ b/Tests/Scribe/LoggableTests.swift @@ -55,32 +55,32 @@ final class LoggableMacroTests: XCTestCase { } func testLoggableClass() { - XCTAssertEqual(LoggableClass._logCategory.name, "LoggableClass") + XCTAssertEqual(LoggableClass.logCategory.name, "LoggableClass") XCTAssertEqual(LoggableClass().log.category.name, "LoggableClass") } func testLoggableStruct() { - XCTAssertEqual(LoggableStruct._logCategory.name, "LoggableStruct") + XCTAssertEqual(LoggableStruct.logCategory.name, "LoggableStruct") XCTAssertEqual(LoggableStruct().log.category.name, "LoggableStruct") } func testLoggableEnum() { - XCTAssertEqual(LoggableEnum._logCategory.name, "LoggableEnum") + XCTAssertEqual(LoggableEnum.logCategory.name, "LoggableEnum") XCTAssertEqual(LoggableEnum.one.log.category.name, "LoggableEnum") } func testLoggableStaticStyle() { - XCTAssertEqual(LoggableClassStatic._logCategory.name, "LoggableClassStatic") + XCTAssertEqual(LoggableClassStatic.logCategory.name, "LoggableClassStatic") XCTAssertEqual(LoggableClassStatic.log.category.name, "LoggableClassStatic") } func testLoggableCustomName() { - XCTAssertEqual(LoggableCustomName._logCategory.name, "CustomName") + XCTAssertEqual(LoggableCustomName.logCategory.name, "CustomName") XCTAssertEqual(LoggableCustomName().log.category.name, "CustomName") } func testLoggablePassedCategory() { - XCTAssertEqual(LoggablePassedCategory._logCategory.name, "PassedInCategory") + XCTAssertEqual(LoggablePassedCategory.logCategory.name, "PassedInCategory") XCTAssertEqual(LoggablePassedCategory().log.category.name, "PassedInCategory") }