Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f296d6b
Removed redundant tests
NSFatalError Nov 21, 2025
13fbbaa
Aligned computed property publishers behavior
NSFatalError Nov 21, 2025
ca967f2
-
NSFatalError Nov 22, 2025
3aa89fa
[SwiftFormat] Applied formatting
NSFatalError Nov 22, 2025
7a241aa
-
NSFatalError Nov 22, 2025
346b74a
-
NSFatalError Nov 22, 2025
79002db
[SwiftFormat] Applied formatting
NSFatalError Nov 22, 2025
c5b8de2
-
NSFatalError Nov 23, 2025
d149d39
-
NSFatalError Nov 23, 2025
ec4fbe9
[SwiftFormat] Applied formatting
NSFatalError Nov 23, 2025
41aab39
-
NSFatalError Nov 23, 2025
1313936
[SwiftFormat] Applied formatting
NSFatalError Nov 23, 2025
0dbc61a
-
NSFatalError Nov 23, 2025
e88cfb2
-
NSFatalError Nov 23, 2025
bcfcd71
-
NSFatalError Nov 23, 2025
852dcef
[SwiftFormat] Applied formatting
NSFatalError Nov 23, 2025
d188403
-
NSFatalError Nov 23, 2025
e001966
-
NSFatalError Nov 23, 2025
2427908
-
NSFatalError Nov 24, 2025
db14844
[SwiftFormat] Applied formatting
NSFatalError Nov 24, 2025
f39be9e
-
NSFatalError Nov 24, 2025
7beacb0
-
NSFatalError Nov 28, 2025
8feca3b
[SwiftFormat] Applied formatting
NSFatalError Nov 28, 2025
65baa00
-
NSFatalError Nov 29, 2025
8ec3528
[SwiftFormat] Applied formatting
NSFatalError Nov 29, 2025
ef264b1
-
NSFatalError Nov 29, 2025
15607ab
[SwiftFormat] Applied formatting
NSFatalError Nov 29, 2025
be9b359
-
NSFatalError Nov 29, 2025
b30f562
-
NSFatalError Nov 29, 2025
c4bddd0
-
NSFatalError Nov 29, 2025
13b2a3f
[SwiftFormat] Applied formatting
NSFatalError Nov 29, 2025
b05b862
-
NSFatalError Nov 29, 2025
95f7369
-
NSFatalError Nov 30, 2025
57b437b
-
NSFatalError Nov 30, 2025
8c98a25
-
NSFatalError Nov 30, 2025
a34a392
-
NSFatalError Nov 30, 2025
def73b1
Added docs
NSFatalError Nov 30, 2025
b19908c
-
NSFatalError Nov 30, 2025
178c3af
-
NSFatalError Nov 30, 2025
06a87f3
-
NSFatalError Nov 30, 2025
0d5c052
[SwiftFormat] Applied formatting
NSFatalError Nov 30, 2025
8af638f
-
NSFatalError Nov 30, 2025
3f29a5a
-
NSFatalError Nov 30, 2025
5b0e2ea
-
NSFatalError Nov 30, 2025
4e65ed4
-
NSFatalError Nov 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true

env:
XCODE_VERSION: "26.1"
XCODE_VERSION: "26.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

GitHub Actions macos-26 runner Xcode 26.0 simulator versions available

💡 Result:

On macos-26 the Xcode and simulator runtimes listed in the runner-images release notes are:

  • Xcode: 26.1.1 (stable) and 26.2 (beta). [1]
  • Installed simulator runtimes / SDKs:
    • iOS Simulator: 26.1, 26.2. [1]
    • tvOS Simulator: 26.1, 26.2. [1]
    • watchOS Simulator: 26.1, 26.2. [1]
    • visionOS Simulator: 26.1, 26.2. [1]

(See the GitHub Actions runner-images macOS-26 release notes for details and exact build paths.) [1][2]

Sources:
[1] actions/runner-images — macOS-26 release notes. [2] actions/runner-images README (Xcode simulator policy).


Critical: Specified Xcode and simulator versions do not exist on macos-26 runners.

The workflow specifies Xcode 26.0 and simulator OS versions 26.0/26.0.1, but GitHub Actions macos-26 runners only provide Xcode 26.1.1 (stable) and 26.2 (beta), with simulator runtimes limited to 26.1 and 26.2 for all platforms (iOS, tvOS, watchOS, visionOS). This configuration will cause CI failures. Update to an available version: either 26.1 or 26.2.

Also applies to: 91-91, 94-94, 97-97, 100-100

🤖 Prompt for AI Agents
.github/workflows/pull-request.yml around lines 15 (and also apply same change
at lines 91, 94, 97, 100): the workflow pins XCODE_VERSION and simulator OS
versions to 26.0/26.0.1 which do not exist on macos-26 runners; update those
values to a supported release (change XCODE_VERSION to "26.1" or "26.2") and
align any simulator runtime variables to "26.1" or "26.2" accordingly so the CI
uses available Xcode and simulator runtimes.


jobs:
prepare:
Expand Down Expand Up @@ -88,16 +88,16 @@ jobs:
destination="platform=macOS,variant=Mac Catalyst"
;;
ios)
destination="platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.1"
destination="platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.0.1"
;;
tvos)
destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.1"
destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.0"
;;
watchos)
destination="platform=watchOS Simulator,name=Apple Watch Series 11 (46mm),OS=26.1"
destination="platform=watchOS Simulator,name=Apple Watch Series 11 (46mm),OS=26.0"
;;
visionos)
destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=26.1"
destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=26.0"
;;
*)
echo "Unknown platform: ${{ matrix.platform }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- '*'

env:
XCODE_VERSION: "26.1"
XCODE_VERSION: "26.0"

jobs:
release:
Expand Down
1 change: 1 addition & 0 deletions .swiftlint.tests.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
disabled_rules:
- function_body_length
- type_body_length
- no_magic_numbers
19 changes: 8 additions & 11 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- async_without_await
# attributes
# attributes (swiftformat)
# balanced_xctest_lifecycle
# closure_body_length
- closure_end_indentation
Expand All @@ -27,7 +27,7 @@ opt_in_rules:
- discouraged_assert
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
# discouraged_optional_boolean
- discouraged_optional_collection
- empty_collection_literal
- empty_count
Expand Down Expand Up @@ -55,9 +55,9 @@ opt_in_rules:
- ibinspectable_in_extension
- identical_operands
- implicit_return
# implicitly_unwrapped_optional
- implicitly_unwrapped_optional
# incompatible_concurrency_annotation
# indentation_width
# indentation_width (swiftformat)
- joined_default_parameter
- last_where
- legacy_multiple
Expand All @@ -67,7 +67,7 @@ opt_in_rules:
- local_doc_comment
- lower_acl_than_parent
# missing_docs
- modifier_order
# modifier_order (swiftformat)
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
Expand Down Expand Up @@ -144,7 +144,7 @@ opt_in_rules:
# vertical_whitespace_opening_braces
- weak_delegate
- xct_specific_matcher
# yoda_condition
# yoda_condition (swiftformat)

analyzer_rules:
- capture_variable
Expand Down Expand Up @@ -173,6 +173,7 @@ identifier_name:
excluded: [id, ui, x, y, z, dx, dy, dz]

line_length:
ignores_multiline_strings: true
ignores_comments: true

nesting:
Expand All @@ -189,10 +190,6 @@ type_contents_order:
order: [[case], [type_alias, associated_type], [subtype], [type_property], [instance_property], [ib_inspectable], [ib_outlet], [initializer], [deinitializer], [type_method], [view_life_cycle_method], [ib_action, ib_segue_action], [other_method], [subscript]]

custom_rules:
global_actor_attribute_order:
name: "Global actor attribute order"
message: "Global actor should be the first attribute."
regex: "(?-s)(@.+[^,\\s]\\s+@.*Actor\\s)"
sendable_attribute_order:
name: "Sendable attribute order"
message: "Sendable should be the first attribute."
Expand All @@ -204,4 +201,4 @@ custom_rules:
empty_line_after_type_declaration:
name: "Empty line after type declaration"
message: "Type declaration should start with an empty line."
regex: "( |^)(actor|class|struct|enum|protocol|extension) (?!var)[^\\{]*? \\{(?!\\s*\\}) *\\n? *\\S"
regex: "( |^)(actor|class|struct|enum|protocol|extension) (?!var)[^\\n\\{]*? \\{(?!\\s*\\}) *\\n? *\\S"
2 changes: 1 addition & 1 deletion Macros/Dependencies/PrincipleMacros
Submodule PrincipleMacros updated 56 files
+5 −5 .github/workflows/pull-request.yml
+1 −1 .github/workflows/release.yml
+2 −2 .swiftlint.yml
+2 −3 Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift
+8 −1 Sources/PrincipleMacros/Builders/Declarations/Common/MemberBuilding.swift
+8 −1 Sources/PrincipleMacros/Builders/Declarations/Common/PeerBuilding.swift
+21 −0 Sources/PrincipleMacros/Builders/Declarations/Members/PropertyDeclAccessorBuilder.swift
+18 −0 Sources/PrincipleMacros/Builders/Declarations/Types/ClassDeclBuilder.swift
+22 −0 Sources/PrincipleMacros/Diagnostics/DiagnosticsError.swift
+6 −4 Sources/PrincipleMacros/Diagnostics/MacroExpansionContext.swift
+62 −6 Sources/PrincipleMacros/Parameters/ParameterExtractor.swift
+39 −7 Sources/PrincipleMacros/Parsers/Common/Parser.swift
+15 −0 Sources/PrincipleMacros/Parsers/Common/ParserResultsCollection.swift
+0 −26 Sources/PrincipleMacros/Parsers/Common/_Parser.swift
+0 −19 Sources/PrincipleMacros/Parsers/Common/_ParserResultsCollection.swift
+2 −2 Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesList.swift
+2 −3 Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift
+2 −2 Sources/PrincipleMacros/Parsers/Properties/PropertiesList.swift
+8 −11 Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift
+37 −6 Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift
+0 −0 Sources/PrincipleMacros/Syntax/Concepts/ClosureTypeSyntax.swift
+3 −3 Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift
+0 −37 Sources/PrincipleMacros/Syntax/Extensions/AttributeListSyntax.swift
+0 −16 Sources/PrincipleMacros/Syntax/Extensions/AttributeSyntax.swift
+58 −0 Sources/PrincipleMacros/Syntax/Extensions/ClassDeclSyntax.swift
+23 −16 Sources/PrincipleMacros/Syntax/Extensions/ExprSyntax.swift
+59 −0 Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+Availability.swift
+117 −0 Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift
+5 −0 Sources/PrincipleMacros/Syntax/Extensions/SyntaxProtocol.swift
+20 −0 Sources/PrincipleMacros/Syntax/Extensions/TypeSyntax.swift
+48 −0 Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+Availability.swift
+41 −0 Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+GlobalActorIsolation.swift
+0 −23 Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax.swift
+42 −0 Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift
+28 −0 Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+GlobalActorIsolation.swift
+50 −0 Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+Specifiers.swift
+0 −61 Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift
+0 −0 Sources/PrincipleMacros/Syntax/Protocols/BasicDeclSyntax.swift
+0 −0 Sources/PrincipleMacros/Syntax/Protocols/StatefulDeclSyntax.swift
+0 −0 Sources/PrincipleMacros/Syntax/Protocols/TypeDeclSyntax.swift
+4 −4 Tests/PrincipleMacrosTests/Builders/EnumCaseCallExprBuilderTests.swift
+2 −2 Tests/PrincipleMacrosTests/Builders/SwitchExprBuilderTests.swift
+161 −98 Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift
+8 −10 Tests/PrincipleMacrosTests/Parsers/EnumCasesParserTests.swift
+2 −4 Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift
+5 −8 Tests/PrincipleMacrosTests/Parsers/PropertiesParserTests.swift
+0 −127 Tests/PrincipleMacrosTests/Syntax/ClosureExprSyntaxTests.swift
+0 −0 Tests/PrincipleMacrosTests/Syntax/Concepts/CamelCaseNotationTests.swift
+4 −4 Tests/PrincipleMacrosTests/Syntax/Concepts/ClosureTypeSyntaxTests.swift
+20 −13 Tests/PrincipleMacrosTests/Syntax/Concepts/GlobalActorIsolationTests.swift
+61 −0 Tests/PrincipleMacrosTests/Syntax/Extensions/AvailabilityTests.swift
+79 −0 Tests/PrincipleMacrosTests/Syntax/Extensions/ClassDeclSyntaxTests.swift
+115 −0 Tests/PrincipleMacrosTests/Syntax/Extensions/ClosureExprSyntaxTests.swift
+1 −4 Tests/PrincipleMacrosTests/Syntax/Extensions/ExprSyntaxTests.swift
+202 −0 Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift
+1 −4 Tests/PrincipleMacrosTests/Syntax/Extensions/TypeSyntaxTests.swift
14 changes: 14 additions & 0 deletions Macros/RelayMacros/Combine/Common/ObservableMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// ObservableMacro.swift
// Relay
//
// Created by Kamil Strzelecki on 23/11/2025.
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import SwiftSyntaxMacros

internal enum ObservableMacro {

static let attribute: AttributeSyntax = "@Observable"
}
25 changes: 25 additions & 0 deletions Macros/RelayMacros/Combine/Common/ObservationIgnoredMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// ObservationIgnoredMacro.swift
// Relay
//
// Created by Kamil Strzelecki on 22/11/2025.
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import SwiftSyntaxMacros

internal enum ObservationIgnoredMacro {

static let attribute: AttributeSyntax = "@ObservationIgnored"
}

extension Property {

var isStoredObservationTracked: Bool {
kind == .stored
&& mutability == .mutable
&& underlying.typeScopeSpecifier == nil
&& underlying.overrideSpecifier == nil
&& !underlying.attributes.contains(like: ObservationIgnoredMacro.attribute)
}
}
187 changes: 187 additions & 0 deletions Macros/RelayMacros/Combine/Common/PropertyPublisherDeclBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//
// PropertyPublisherDeclBuilder.swift
// Relay
//
// Created by Kamil Strzelecki on 12/01/2025.
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import SwiftSyntaxMacros

internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {

let declaration: ClassDeclSyntax
let properties: PropertiesList
let trimmedSuperclassType: TypeSyntax?
let preferredGlobalActorIsolation: GlobalActorIsolation?

func build() -> [DeclSyntax] {
[
"""
\(inheritedGlobalActorIsolation)\(inheritedAccessControlLevelAllowingOpen)\(inheritedFinalModifier)\
class PropertyPublisher: \(inheritanceClause()) {

private final unowned let object: \(trimmedType)

\(objectWillChangeDidChangePublishers())

\(initializer())

\(deinitializer())

\(storedPropertiesPublishers().formatted())

\(computedPropertiesPublishers().formatted())

\(memoizedPropertiesPublishers().formatted())
}
"""
]
}

private func inheritanceClause() -> TypeSyntax {
if let trimmedSuperclassType {
"\(trimmedSuperclassType).PropertyPublisher"
} else {
"Relay.AnyPropertyPublisher"
}
}

private func objectWillChangeDidChangePublishers() -> MemberBlockItemListSyntax {
let notation = CamelCaseNotation(string: trimmedType.description)
let prefix = notation.joined(as: .lowerCamelCase)

return """
\(inheritedAccessControlLevel)final var \
\(raw: prefix)WillChange: some Publisher<\(trimmedType), Never> {
willChange.map { [unowned object] _ in
object
}
}

\(inheritedAccessControlLevel)final var \
\(raw: prefix)DidChange: some Publisher<\(trimmedType), Never> {
didChange.map { [unowned object] _ in
object
}
}
"""
}

private func initializer() -> MemberBlockItemListSyntax {
"""
\(inheritedAccessControlLevel)init(object: \(trimmedType)) {
self.object = object
super.init(object: object)
}
"""
}

private func deinitializer() -> MemberBlockItemListSyntax {
"""
\(inheritedGlobalActorIsolation)deinit {
\(storedPropertiesSubjectsFinishCalls().formatted())
}
"""
}

@CodeBlockItemListBuilder
private func storedPropertiesSubjectsFinishCalls() -> CodeBlockItemListSyntax {
for property in properties.all where property.isStoredPublisherTracked {
let call = storedPropertySubjectFinishCall(for: property)
if let ifConfigCall = property.underlying.applyingEnclosingIfConfig(to: call) {
ifConfigCall
} else {
call
}
}
}

private func storedPropertySubjectFinishCall(for property: Property) -> CodeBlockItemListSyntax {
"_\(property.trimmedName).send(completion: .finished)"
}

@MemberBlockItemListBuilder
private func storedPropertiesPublishers() -> MemberBlockItemListSyntax {
for property in properties.all where property.isStoredPublisherTracked {
let publisher = storedPropertyPublisher(for: property)
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
}
}

private func storedPropertyPublisher(for property: Property) -> MemberBlockItemListSyntax {
// Stored properties cannot be made potentially unavailable
let accessControlLevel = AccessControlLevel.forSibling(of: property.underlying)
let name = property.trimmedName
let type = property.inferredType

return """
fileprivate final let _\(name) = PassthroughSubject<\(type), Never>()
\(accessControlLevel)final var \(name): some Publisher<\(type), Never> {
_storedPropertyPublisher(_\(name), for: \\.\(name), object: object)
}
"""
}

@MemberBlockItemListBuilder
private func computedPropertiesPublishers() -> MemberBlockItemListSyntax {
for property in properties.all where property.isComputedPublisherTracked {
let publisher = computedPropertyPublisher(for: property)
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
}
}

private func computedPropertyPublisher(for property: Property) -> MemberBlockItemListSyntax {
let accessControlLevel = AccessControlLevel.forSibling(of: property.underlying)
let availability = property.availability?.trimmed.withTrailingNewline
let name = property.trimmedName
let type = property.inferredType

return """
\(availability)\(accessControlLevel)final var \(name): some Publisher<\(type), Never> {
_computedPropertyPublisher(for: \\.\(name), object: object)
}
"""
}

@MemberBlockItemListBuilder
private func memoizedPropertiesPublishers() -> MemberBlockItemListSyntax {
for member in declaration.memberBlock.members {
if let extractionResult = MemoizedMacro.extract(from: member.decl) {
let declaration = extractionResult.declaration

if !declaration.attributes.contains(like: PublisherIgnoredMacro.attribute) {
let publisher = memoizedPropertyPublisher(for: extractionResult)
if let ifConfigPublisher = declaration.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
}
}
}
}

private func memoizedPropertyPublisher(
for extractionResult: MemoizedMacro.ExtractionResult
) -> MemberBlockItemListSyntax {
let accessControlLevel = extractionResult.preferredAccessControlLevel?.inheritedBySibling()
let availability = extractionResult.declaration.availability?.trimmed.withTrailingNewline
let name = extractionResult.propertyName
let type = extractionResult.trimmedReturnType

return """
\(availability)\(accessControlLevel)final var \(raw: name): some Publisher<\(type), Never> {
_computedPropertyPublisher(for: \\.\(raw: name), object: object)
}
"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import SwiftSyntaxMacros
internal struct PublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {

let declaration: ClassDeclSyntax
let properties: PropertiesList
let trimmedSuperclassType: TypeSyntax?

func build() -> [DeclSyntax] {
[
"""
private final lazy var _publisher = PropertyPublisher(object: self)
""",
"""
/// A ``PropertyPublisher`` which exposes `Combine` publishers for all mutable
/// or computed instance properties of this object.
Expand All @@ -23,7 +26,9 @@ internal struct PublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
/// the original object has been deallocated may result in a crash. Always access it directly
/// through the object that exposes it.
///
\(inheritedAccessControlLevel)private(set) lazy var publisher = PropertyPublisher(object: self)
\(inheritedOverrideModifier)\(inheritedAccessControlLevelAllowingOpen)var publisher: PropertyPublisher {
_publisher
}
"""
]
}
Expand Down
Loading
Loading