From 14ea244f165a950f430a332c7379e2c2e6803cbb Mon Sep 17 00:00:00 2001 From: Patrick Gatewood Date: Fri, 28 Nov 2025 14:01:54 -0500 Subject: [PATCH 1/2] Fix package resolution CLose #5 --- .github/workflows/run_tests.yaml | 39 ++----- .../.gitignore | 3 +- ConsumptionTest/Package.resolved | 14 +++ ConsumptionTest/Package.swift | 31 ++++++ ConsumptionTest/README.md | 28 +++++ .../Sources/InvalidCode/main.swift | 20 ++++ ConsumptionTest/Sources/ValidCode/main.swift | 25 +++++ ConsumptionTest/Tests/test-compilation.sh | 28 +++++ Package.resolved | 14 +++ Package.swift | 104 +++++++++++------- .../CompilationTests/Package.swift | 23 ---- PredicateBuilder/CompilationTests/README.md | 6 - .../Sources/CompilationTests/main.swift | 10 -- .../Tests/test-compilation.sh | 13 --- .../PredicateBuilderMacro.swift | 6 + PredicateBuilderExample/main.swift | 3 - .../PredicateBuilderMacroMacros.swift | 35 +++--- .../PredicateBuilderMacroTests.swift | 2 +- ...ge_CFD05A3B-521E-4AE5-9212-CD1A8931A989.md | 3 + 19 files changed, 267 insertions(+), 140 deletions(-) rename {PredicateBuilder/CompilationTests => ConsumptionTest}/.gitignore (72%) create mode 100644 ConsumptionTest/Package.resolved create mode 100644 ConsumptionTest/Package.swift create mode 100644 ConsumptionTest/README.md create mode 100644 ConsumptionTest/Sources/InvalidCode/main.swift create mode 100644 ConsumptionTest/Sources/ValidCode/main.swift create mode 100755 ConsumptionTest/Tests/test-compilation.sh create mode 100644 Package.resolved delete mode 100644 PredicateBuilder/CompilationTests/Package.swift delete mode 100644 PredicateBuilder/CompilationTests/README.md delete mode 100644 PredicateBuilder/CompilationTests/Sources/CompilationTests/main.swift delete mode 100755 PredicateBuilder/CompilationTests/Tests/test-compilation.sh create mode 100644 PredicateBuilder/Sources/PredicateBuilder/PredicateBuilderMacro.swift create mode 100644 changelogs/unreleased/fixed_fix_spm_package_CFD05A3B-521E-4AE5-9212-CD1A8931A989.md diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 012fc6a..c120d3a 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -9,40 +9,21 @@ concurrency: cancel-in-progress: true jobs: predicate-builder-package-tests: - name: XCTest | Xcode ${{ matrix.xcode-version }} on ${{ matrix.os }} - strategy: - matrix: - os: [macos-13] - xcode-version: ["14.2", "14.3"] - env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer" - runs-on: ${{ matrix.os }} + name: XCTest | macOS Latest + runs-on: macos-latest defaults: run: - working-directory: ./PredicateBuilder + working-directory: ./ steps: - uses: actions/checkout@v3 - name: Build Swift Package run: swift build - name: Run Tests run: swift test - - name: Type Inference Tests | Xcode ${{ matrix.xcode-version }} on ${{ matrix.os }} - working-directory: ./PredicateBuilder/CompilationTests/Tests/ - run: ./test-compilation.sh - # predicate-builder-macro-package-tests: - # name: Test PredicateBuilderMacro Package | Xcode ${{ matrix.xcode-version }} on ${{ matrix.os }} - # runs-on: macos-13 - # strategy: - # matrix: - # xcode-version: ["15.0.0"] - # env: - # DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer" - # defaults: - # run: - # working-directory: ./PredicateBuilderMacro - # steps: - # - uses: actions/checkout@v3 - # - name: Build Swift Package - # run: swift build - # - name: Run Tests - # run: swift test \ No newline at end of file + - name: Public API Consumption Tests + working-directory: ./ConsumptionTest + run: | + echo "Testing valid code compiles..." + swift build --target ValidCode + echo "Testing invalid code fails to compile..." + ./Tests/test-compilation.sh \ No newline at end of file diff --git a/PredicateBuilder/CompilationTests/.gitignore b/ConsumptionTest/.gitignore similarity index 72% rename from PredicateBuilder/CompilationTests/.gitignore rename to ConsumptionTest/.gitignore index 3b29812..0023a53 100644 --- a/PredicateBuilder/CompilationTests/.gitignore +++ b/ConsumptionTest/.gitignore @@ -1,9 +1,8 @@ .DS_Store /.build /Packages -/*.xcodeproj xcuserdata/ DerivedData/ -.swiftpm/config/registries.json +.swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc diff --git a/ConsumptionTest/Package.resolved b/ConsumptionTest/Package.resolved new file mode 100644 index 0000000..e75c36f --- /dev/null +++ b/ConsumptionTest/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 2 +} diff --git a/ConsumptionTest/Package.swift b/ConsumptionTest/Package.swift new file mode 100644 index 0000000..3471770 --- /dev/null +++ b/ConsumptionTest/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ConsumptionTest", + platforms: [ + .iOS(.v16), + .macOS(.v12), + ], + dependencies: [ + .package(path: "..") + ], + targets: [ + .executableTarget( + name: "ValidCode", + dependencies: [ + .product(name: "PredicateBuilder", package: "predicate-builder"), + ], + path: "Sources/ValidCode" + ), + .executableTarget( + name: "InvalidCode", + dependencies: [ + .product(name: "PredicateBuilder", package: "predicate-builder"), + ], + path: "Sources/InvalidCode" + ), + ] +) diff --git a/ConsumptionTest/README.md b/ConsumptionTest/README.md new file mode 100644 index 0000000..6e6543c --- /dev/null +++ b/ConsumptionTest/README.md @@ -0,0 +1,28 @@ +# ConsumptionTest + +This package tests that PredicateBuilder can be consumed correctly through its public API. + +## Purpose + +This test verifies both positive and negative compilation cases: + +**Positive Tests (ValidCode target):** +- Users only need to `import PredicateBuilder` (single import) +- The macro (`#PredicateBuilder`) is automatically available +- The result builder (`@PredicateBuilder`) works correctly +- All types and functionality are accessible through the single import + +**Negative Tests (InvalidCode target):** +- Type checking works correctly - invalid code fails to compile +- Compiler errors are appropriate and prevent runtime crashes +- Type constraints are enforced through the public API + +## Why This Exists + +The main test suite uses `@testable import`, which bypasses public API boundaries and allows importing internal modules directly. This test uses only the public API, ensuring that: + +1. The package structure is correct +2. Re-exports work as intended +3. The public API contract is maintained +4. Breaking changes to the public API are caught +5. Type checking works correctly through the public API diff --git a/ConsumptionTest/Sources/InvalidCode/main.swift b/ConsumptionTest/Sources/InvalidCode/main.swift new file mode 100644 index 0000000..dc8da23 --- /dev/null +++ b/ConsumptionTest/Sources/InvalidCode/main.swift @@ -0,0 +1,20 @@ +import Foundation +import PredicateBuilder + +// MARK: - Invalid Code Tests +// These should NOT compile - they test type checking behavior +// This file is intentionally invalid and should cause compilation to fail + +@main +struct InvalidCode { + static func main() { + @PredicateBuilder var valid: NSPredicate { + \NSString.boolValue == true + } + + @PredicateBuilder var shouldNotCompile: NSPredicate { + \NSString.boolValue == true // Wrong type - should fail compilation + } + } +} + diff --git a/ConsumptionTest/Sources/ValidCode/main.swift b/ConsumptionTest/Sources/ValidCode/main.swift new file mode 100644 index 0000000..f154443 --- /dev/null +++ b/ConsumptionTest/Sources/ValidCode/main.swift @@ -0,0 +1,25 @@ +import CoreData +import PredicateBuilder + +// MARK: - Valid Code Tests +// These should compile successfully when using the public API + +class Test: NSManagedObject { + @NSManaged var name: String +} + +@main +struct ValidCode { + static func main() { + // Test macro usage - if this compiles, the macro is available via public API + let _: NSPredicate = #PredicateBuilder { + \.name == "hello" + } + + // Test result builder usage - if this compiles, the builder is available via public API + @PredicateBuilder + var _unused: NSPredicate { + \.name == "hello" + } + } +} diff --git a/ConsumptionTest/Tests/test-compilation.sh b/ConsumptionTest/Tests/test-compilation.sh new file mode 100755 index 0000000..250ecb2 --- /dev/null +++ b/ConsumptionTest/Tests/test-compilation.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Test that invalid code fails to compile with the expected error +# This ensures type checking is working correctly through the public API + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR/.." + +echo "Testing that invalid code fails to compile..." +BUILD_OUTPUT=$(swift build --target InvalidCode 2>&1) +BUILD_EXIT_CODE=$? + +echo -e "Build output:\n $BUILD_OUTPUT" + +if [ $BUILD_EXIT_CODE -eq 0 ]; then + echo "ERROR: Invalid code compiled successfully! Type checking may not be working correctly." + exit 1 +fi + +if [[ $BUILD_OUTPUT != *"'PredicateBuilder' requires that 'NSString' inherit from 'NSManagedObject'"* ]]; then + echo "ERROR: Compilation failed, but for the wrong reason!" + echo "Expected error message about 'NSString' inheriting from 'NSManagedObject'" + echo "Make sure the compiler is not inferring the root type to be 'NSManagedObject'" + exit 1 +fi + +echo "✅ The compiler failed in the expected manner. The PredicateBuilder's type checking is working correctly through the public API." + diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ae921e1 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 84b010e..c87bc54 100644 --- a/Package.swift +++ b/Package.swift @@ -1,7 +1,8 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +import CompilerPluginSupport let package = Package( name: "PredicateBuilder", @@ -20,60 +21,87 @@ let package = Package( ) ], dependencies: [ - .package(path: "PredicateBuilderCore"), - .package(path: "PredicateBuilderTestData") - ].withMacroDependencyIfPossible(), + .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1") + ], targets: [ + .target( + name: "PredicateBuilderCore", + path: "PredicateBuilderCore/Sources/PredicateBuilderCore" + ), .target( name: "PredicateBuilder", dependencies: [ - .product(name: "PredicateBuilderCore", package: "PredicateBuilderCore") - ].withMacroDependencyIfPossible(), + .target(name: "PredicateBuilderCore"), + .target(name: "PredicateBuilderMacro") + ], path: "PredicateBuilder/Sources" ), + .macro( + name: "PredicateBuilderMacroMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .target(name: "PredicateBuilderCore") + ], + path: "PredicateBuilderMacro/Sources/PredicateBuilderMacroMacros" + ), + .target( + name: "PredicateBuilderMacro", + dependencies: [ + .target(name: "PredicateBuilderCore"), + "PredicateBuilderMacroMacros" + ], + path: "PredicateBuilderMacro/Sources/PredicateBuilderMacro" + ), + .target( + name: "PredicateBuilderTestData", + path: "PredicateBuilderTestData/Sources/PredicateBuilderTestData" + ), .executableTarget( name: "PredicateBuilderExample", dependencies: [ .target(name: "PredicateBuilder"), - .product(name: "PredicateBuilderTestData", package: "PredicateBuilderTestData") - ].withMacroDependencyIfPossible(), + .target(name: "PredicateBuilderTestData") + ], path: "PredicateBuilderExample" ), + .executableTarget( + name: "PredicateBuilderMacroClient", + dependencies: [ + .target(name: "PredicateBuilderMacro"), + .target(name: "PredicateBuilderTestData") + ], + path: "PredicateBuilderMacro/PredicateBuilderMacroClient" + ), .testTarget( name: "PredicateBuilderTests", dependencies: [ .target(name: "PredicateBuilder"), - .product(name: "PredicateBuilderTestData", package: "PredicateBuilderTestData") - ].withMacroDependencyIfPossible(), + .target(name: "PredicateBuilderTestData") + ], path: "PredicateBuilder/Tests" ), + .testTarget( + name: "PredicateBuilderCoreTests", + dependencies: [ + .target(name: "PredicateBuilderCore") + ], + path: "PredicateBuilderCore/Tests/PredicateBuilderCoreTests" + ), + .testTarget( + name: "PredicateBuilderMacroTests", + dependencies: [ + "PredicateBuilderMacroMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ], + path: "PredicateBuilderMacro/Tests/PredicateBuilderMacroTests" + ), + .testTarget( + name: "PredicateBuilderTestDataTests", + dependencies: [ + .target(name: "PredicateBuilderTestData") + ], + path: "PredicateBuilderTestData/Tests/PredicateBuilderTestDataTests" + ), ] ) - -extension Array where Element == Target.Dependency { - func withMacroDependencyIfPossible() -> Self { -#if(swift(<5.9)) - return self -#else - var array = self - array.append( - .product(name: "PredicateBuilderMacro", package: "PredicateBuilderMacro") - ) - return array -#endif - } -} - -extension Array where Element == Package.Dependency { - func withMacroDependencyIfPossible() -> Self { -#if(swift(<5.9)) - return self -#else - var array = self - array.append( - .package(path: "PredicateBuilderMacro") - ) - return array -#endif - } -} diff --git a/PredicateBuilder/CompilationTests/Package.swift b/PredicateBuilder/CompilationTests/Package.swift deleted file mode 100644 index 4351409..0000000 --- a/PredicateBuilder/CompilationTests/Package.swift +++ /dev/null @@ -1,23 +0,0 @@ -// swift-tools-version: 5.7 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "CompilationTests", - platforms: [ - .macOS(.v12) - ], - dependencies: [ - .package(name: "PredicateBuilderCore", path: "../../PredicateBuilderCore") - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .executableTarget( - name: "CompilationTests", - dependencies: [ - .product(name: "PredicateBuilderCore", package: "PredicateBuilderCore") - ]) - ] -) diff --git a/PredicateBuilder/CompilationTests/README.md b/PredicateBuilder/CompilationTests/README.md deleted file mode 100644 index 839e6fd..0000000 --- a/PredicateBuilder/CompilationTests/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# CompilationTests - -This package contains code that, when the PredicateBuilder is working as-intended, should not compile. It exists separately from the package's tests since the code needs to not compile in order to be correct. - -## Why is this needed? -Swift is smart. When the type constraints specified by the result builder are not tight enough, Swift will infer the generic type of the predicate builder to be of type `NSObject`. Normally, type inference is wonderful, but in this case, we want to ensure that we constrain things more tightly. This is because NSPredicate crashes at runtime when predicates aren't constructed correctly--and this library aims to define away those problem areas using Swift's type system diff --git a/PredicateBuilder/CompilationTests/Sources/CompilationTests/main.swift b/PredicateBuilder/CompilationTests/Sources/CompilationTests/main.swift deleted file mode 100644 index fa47cb7..0000000 --- a/PredicateBuilder/CompilationTests/Sources/CompilationTests/main.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import PredicateBuilderCore - -@PredicateBuilder var valid: NSPredicate { - \NSString.boolValue == true -} - -@PredicateBuilder var shouldNotCompile: NSPredicate { - \NSString.boolValue == true -} diff --git a/PredicateBuilder/CompilationTests/Tests/test-compilation.sh b/PredicateBuilder/CompilationTests/Tests/test-compilation.sh deleted file mode 100755 index 01ec6bf..0000000 --- a/PredicateBuilder/CompilationTests/Tests/test-compilation.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -cd .. - -BUILD_OUTPUT=$(swift build) -echo -e "Build output:\n $BUILD_OUTPUT" - -if [[ $BUILD_OUTPUT != *"'PredicateBuilder' requires that 'NSString' inherit from 'NSManagedObject'"* ]]; then - echo "Compiling main.swift either succeeded, or failed for the wrong reason! Make sure the compiler is not inferring the root type to be 'NSManagedObject'" - exit 1 -fi - -echo "The compiler failed in the expected manner. The PredicateBuilder's type checking is working correctly ✅" diff --git a/PredicateBuilder/Sources/PredicateBuilder/PredicateBuilderMacro.swift b/PredicateBuilder/Sources/PredicateBuilder/PredicateBuilderMacro.swift new file mode 100644 index 0000000..6696db5 --- /dev/null +++ b/PredicateBuilder/Sources/PredicateBuilder/PredicateBuilderMacro.swift @@ -0,0 +1,6 @@ +@_exported import PredicateBuilderCore + +#if swift(>=5.9) +@_exported import PredicateBuilderMacro +#endif + diff --git a/PredicateBuilderExample/main.swift b/PredicateBuilderExample/main.swift index 9d65c98..bf209cb 100644 --- a/PredicateBuilderExample/main.swift +++ b/PredicateBuilderExample/main.swift @@ -1,6 +1,5 @@ import CoreData import PredicateBuilder -import PredicateBuilderCore import PredicateBuilderTestData // A bunch of predicate examples. @@ -166,8 +165,6 @@ logResults(for: anyPredicate) // MARK: - Swift 5.9 playground #if swift(>=5.9) -import PredicateBuilderMacro - if #available(macOS 14.0, iOS 17.0, *) { // MARK: Trouble with #Predicate macro diff --git a/PredicateBuilderMacro/Sources/PredicateBuilderMacroMacros/PredicateBuilderMacroMacros.swift b/PredicateBuilderMacro/Sources/PredicateBuilderMacroMacros/PredicateBuilderMacroMacros.swift index 94b3543..85aeff9 100644 --- a/PredicateBuilderMacro/Sources/PredicateBuilderMacroMacros/PredicateBuilderMacroMacros.swift +++ b/PredicateBuilderMacro/Sources/PredicateBuilderMacroMacros/PredicateBuilderMacroMacros.swift @@ -39,17 +39,18 @@ public struct PredicateBuilderMacro: ExpressionMacro { ) throws -> ExprSyntax { guard let genericArguments = node.genericArguments, let genericType = genericArguments.arguments.first else { - let noSpecializationError = Diagnostic( - node: Syntax(node), - message: PredicateBuilderMacroDiagnostic.noSpecialization + context.diagnose( + Diagnostic( + node: Syntax(node), + message: PredicateBuilderMacroDiagnostic.noSpecialization + ) ) - context.diagnose(noSpecializationError) return "" } - + // Diagnostics are not needed for too many specializations because the // type system catches that error when the macro expands - + let buildBlockBody: CodeBlockItemListSyntax = if let statements = node.trailingClosure?.statements { statements @@ -58,18 +59,22 @@ public struct PredicateBuilderMacro: ExpressionMacro { // already correctly handles empty bodies CodeBlockItemListSyntax([]) } - + // I'm not sure why `buildBlockBody` comes with the leading trivia "\n", // but we trim it here so that the expansion looks better to our fellow debuggers. let trimmedBody = buildBlockBody.trimmed - return """ - { - @PredicateBuilder<\(genericType)> var predicate: AnyTypedPredicate<\(genericType)> { - \(trimmedBody) - } - return predicate - }() - """ + let genericTypeTrimmed = genericType.trimmed + + return ExprSyntax( + stringLiteral: """ + { + @PredicateBuilder<\(genericTypeTrimmed.description)> var predicate: AnyTypedPredicate<\(genericTypeTrimmed.description)> { + \(trimmedBody.description) + } + return predicate + }() + """ + ) } } #endif diff --git a/PredicateBuilderMacro/Tests/PredicateBuilderMacroTests/PredicateBuilderMacroTests.swift b/PredicateBuilderMacro/Tests/PredicateBuilderMacroTests/PredicateBuilderMacroTests.swift index b14a35b..d5e3134 100644 --- a/PredicateBuilderMacro/Tests/PredicateBuilderMacroTests/PredicateBuilderMacroTests.swift +++ b/PredicateBuilderMacro/Tests/PredicateBuilderMacroTests/PredicateBuilderMacroTests.swift @@ -20,7 +20,7 @@ final class PredicateBuilderMacroTests: XCTestCase { expandedSource: #""" { - @PredicateBuilder var predicate: AnyTypedPredicate { + var predicate: AnyTypedPredicate { \Spaceship.isReal } return predicate diff --git a/changelogs/unreleased/fixed_fix_spm_package_CFD05A3B-521E-4AE5-9212-CD1A8931A989.md b/changelogs/unreleased/fixed_fix_spm_package_CFD05A3B-521E-4AE5-9212-CD1A8931A989.md new file mode 100644 index 0000000..a83509e --- /dev/null +++ b/changelogs/unreleased/fixed_fix_spm_package_CFD05A3B-521E-4AE5-9212-CD1A8931A989.md @@ -0,0 +1,3 @@ +### Fixed +- Fix SPM package resolution +- Bundle macro with Swift package From 7eb2a14af5872ef74865bf485ef09b5327921cd3 Mon Sep 17 00:00:00 2001 From: Patrick Gatewood Date: Fri, 28 Nov 2025 14:24:11 -0500 Subject: [PATCH 2/2] Set minimal workflow permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/run_tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index c120d3a..baaf76a 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -1,4 +1,6 @@ name: Build And Test PredicateBuilder +permissions: + contents: read on: push: