├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── MacroCows │ └── MacroCows.swift ├── MacroCowsClient │ └── main.swift └── MacroCowsMacros │ ├── MacroCow.swift │ ├── MacroDiagnostic.swift │ └── StaticEvaluation.swift ├── Tests └── MacroCowsTests │ └── MacroCowsTests.swift └── images └── MacroCows.png /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "19 9 * * 1" 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - swift:5.9-focal 17 | container: ${{ matrix.image }} 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v3 21 | - name: Build Swift Debug Package 22 | run: swift build -c debug 23 | - name: Build Swift Release Package 24 | run: swift build -c release 25 | - name: Run Tests 26 | run: swift test 27 | nextstep: 28 | runs-on: macos-13 29 | steps: 30 | - name: Select latest available Xcode 31 | uses: maxim-lobanov/setup-xcode@v1 32 | with: 33 | xcode-version: '15.0.0' 34 | - name: Checkout Repository 35 | uses: actions/checkout@v2 36 | - name: Build Swift Debug Package 37 | run: swift build -c debug 38 | - name: Build Swift Release Package 39 | run: swift build -c release 40 | - name: Run Tests 41 | run: swift test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .swiftpm 93 | Package.resolved 94 | .DS_Store 95 | 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Helge Heß 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | import CompilerPluginSupport 4 | 5 | let package = Package( 6 | name: "MacroCows", 7 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 8 | products: [ 9 | .library( 10 | name: "MacroCows", 11 | targets: ["MacroCows"] 12 | ), 13 | .executable( 14 | name: "MacroCowsClient", 15 | targets: ["MacroCowsClient"] 16 | ), 17 | ], 18 | dependencies: [ 19 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax 20 | .package(url: "https://github.com/apple/swift-syntax.git", 21 | from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), 22 | .package(url: "https://github.com/AlwaysRightInstitute/cows.git", 23 | from: "1.0.10") 24 | ], 25 | targets: [ 26 | .macro( 27 | name: "MacroCowsMacros", 28 | dependencies: [ 29 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 30 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 31 | .product(name: "cows", package: "cows") 32 | ] 33 | ), 34 | 35 | .target(name: "MacroCows", dependencies: ["MacroCowsMacros"]), 36 | 37 | // A client of the library, which is able to use the macro in its own code. 38 | .executableTarget(name: "MacroCowsClient", dependencies: ["MacroCows"]), 39 | 40 | // A test target used to develop the macro implementation. 41 | .testTarget( 42 | name: "MacroCowsTests", 43 | dependencies: [ 44 | "MacroCowsMacros", 45 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 46 | ] 47 | ), 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MacroCows 2 | 4 |

5 | 6 | > 400+ ASCII 🐮s 7 | 8 | ![](https://cloud.githubusercontent.com/assets/170270/13090998/a9cdd6b0-d52b-11e5-83ec-614143c9a3bb.png) 9 | 10 | What is it? A Swift 5.9 compiler plugin that provides the `#🐮` [Swift macro](https://developer.apple.com/documentation/swift/applying-macros). 11 | It replaces the cow mentioned in the macro w/ one of the ASCII cows 12 | provided by the Swift [cows](https://github.com/AlwaysRightInstitute/cows) package. 13 | 14 | ### Usage: 15 | ```swift 16 | let compilerCow = #🐮("compiler") 17 | print(compilerCow) 18 | ``` 19 | If no cow matching the string is available, Xcode will produce an error: 20 | ![Xcode using MacroCows](images/MacroCows.png) 21 | 22 | *Requires*: Xcode 15beta+. 23 | 24 | ### Related 25 | 26 | - Apps: 27 | - [CodeCows](https://zeezide.de/en/products/codecows/) for macOS, includes a cows service and Xcode editor extension 28 | - [ASCII Cows](https://zeezide.de/en/products/asciicows/) for iOS, includes a cows message app extension 29 | - Swift [cows](https://github.com/AlwaysRightInstitute/cows) package 30 | - Swift Macros: 31 | - [Intro to Swift macros](https://developer.apple.com/documentation/swift/applying-macros) 32 | - WWDC 2023: [Expand on Swift macros](https://developer.apple.com/videos/play/wwdc2023/10167) 33 | - Original: 34 | - [cows](https://github.com/sindresorhus/cows) - Node.js cows, the original 35 | - [vaca](https://github.com/sindresorhus/vaca) - Get a random ASCII cow 🐮 36 | - [cows-docker](https://github.com/alexellis/cows-docker) - ASCII cows on Docker 37 | 38 | ### License 39 | 40 | MIT © [Sindre Sorhus](http://sindresorhus.com) 41 | Noze.io port: MIT © [ZeeZide GmbH](http://zeezide.de) 42 | 43 | ### Who 44 | 45 | **Macro** is brought to you by 46 | [ZeeZide](http://zeezide.de). 47 | We like 48 | [feedback](https://twitter.com/ar_institute), 49 | GitHub stars, 50 | cool [contract work](http://zeezide.com/en/services/services.html), 51 | presumably any form of praise you can think of. 52 | -------------------------------------------------------------------------------- /Sources/MacroCows/MacroCows.swift: -------------------------------------------------------------------------------- 1 | /// A Macro which takes a String expression 2 | /// and produces a String containing an ASCII cow matching 3 | /// the query. For example 4 | /// 5 | /// #🐮("1989") 6 | /// 7 | /// will expand to 8 | /// ``` 9 | /// (__) (__) 10 | /// (oo) (oo) 11 | /// ______________\/______ ____ /-------\/ __ 12 | /// /___/___/___/___/___/_/| /___/| / | || _/_/| 13 | /// |___|___|___|___|___|_|| |__||/|* //-----|||_|_|| 14 | /// |_|___|___|___|___|___|| |_|__|/|^^ ^/|___|| 15 | /// |___|___|___|___|___|_|/ |___|_|/ |___|_|/ 16 | /// Cow in the GDR before... ...and after 9-Nov-1989 17 | /// ``` 18 | @freestanding(expression) 19 | public macro 🐮(_ value: String) -> String = 20 | #externalMacro(module: "MacroCowsMacros", type: "MacroCow") 21 | -------------------------------------------------------------------------------- /Sources/MacroCowsClient/main.swift: -------------------------------------------------------------------------------- 1 | import MacroCows 2 | 3 | let cow1989 = #🐮("1989") 4 | let compilerCow = #🐮("compiler") 5 | 6 | #if false // will produce a static error, because no tasty cow exists 7 | let missingCow = #🐮("Tasty Cow") 8 | #endif 9 | 10 | // (__) 11 | // / .\/. ______ 12 | // | /\_| | \ 13 | // | |___ | | 14 | // | ---@ |_______| 15 | // * | | ---- | | 16 | // \ | |_____ 17 | // \|________| 18 | // CompuCow Discovers Bug in Compiler 19 | print(compilerCow) 20 | -------------------------------------------------------------------------------- /Sources/MacroCowsMacros/MacroCow.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | import cows 7 | import Foundation 8 | 9 | /// Implementation of the `cow` macro, which takes a String expression 10 | /// and produces a String containing an ASCII matching the query 11 | /// and the source code that produced the value. For example 12 | /// 13 | /// #🐮("1989") 14 | /// 15 | /// will expand to 16 | /// ``` 17 | /// (__) (__) 18 | /// (oo) (oo) 19 | /// ______________\/______ ____ /-------\/ __ 20 | /// /___/___/___/___/___/_/| /___/| / | || _/_/| 21 | /// |___|___|___|___|___|_|| |__||/|* //-----|||_|_|| 22 | /// |_|___|___|___|___|___|| |_|__|/|^^ ^/|___|| 23 | /// |___|___|___|___|___|_|/ |___|_|/ |___|_|/ 24 | /// Cow in the GDR before... ...and after 9-Nov-1989 25 | /// ``` 26 | public struct MacroCow: ExpressionMacro { 27 | 28 | public static func expansion( 29 | of node: some FreestandingMacroExpansionSyntax, 30 | in context: some MacroExpansionContext 31 | ) -> ExprSyntax 32 | { 33 | guard let argument = node.argumentList.first?.expression else { 34 | let message = MacroDiagnostic.missingArgument 35 | context.diagnose(Diagnostic(node: Syntax(node), message: message)) 36 | return "\(literal: "")" 37 | } 38 | 39 | let needle : String 40 | do { 41 | needle = try argument.evaluateAsString() 42 | } 43 | catch { 44 | let message = (error as? MacroDiagnostic) ?? .missingArgument 45 | context.diagnose(Diagnostic(node: Syntax(node), message: message)) 46 | return "\(literal: "")" 47 | } 48 | 49 | guard let cow = lookupCow(needle) else { 50 | context.diagnose(Diagnostic(node: Syntax(node), 51 | message: MacroDiagnostic.foundNoMatchingCow)) 52 | return "\(literal: vaca())" 53 | } 54 | 55 | return "\(literal: cow)" 56 | } 57 | 58 | private static func lookupCow(_ needle: String) -> String? { 59 | let lower = needle.lowercased() 60 | return cows.allCows.first(where: { $0.range(of: needle) != nil }) 61 | ?? cows.allCows.first(where: { $0.lowercased().range(of:lower) != nil }) 62 | } 63 | } 64 | 65 | @main 66 | struct MacroCowsPlugin: CompilerPlugin { 67 | let providingMacros: [Macro.Type] = [ MacroCow.self ] 68 | } 69 | -------------------------------------------------------------------------------- /Sources/MacroCowsMacros/MacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | // Created by Helge Heß on 12.06.23. 2 | 3 | import SwiftDiagnostics 4 | 5 | enum MacroDiagnostic: String, DiagnosticMessage, Swift.Error { 6 | 7 | case missingArgument 8 | case unsupportedExpression 9 | case couldNotGetStringValue 10 | case foundNoMatchingCow 11 | 12 | var message: String { 13 | switch self { 14 | case .missingArgument: 15 | "Missing Argument" 16 | case .unsupportedExpression: 17 | "Not a supported expression type, use literal Strings." 18 | case .couldNotGetStringValue: 19 | "Could not extract string value from expression." 20 | case .foundNoMatchingCow: 21 | "There is no cow which matches the provided string!" 22 | } 23 | } 24 | 25 | var diagnosticID: SwiftDiagnostics.MessageID { 26 | .init(domain: "MacroCows", id: rawValue) 27 | } 28 | 29 | var severity: SwiftDiagnostics.DiagnosticSeverity { 30 | .error 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/MacroCowsMacros/StaticEvaluation.swift: -------------------------------------------------------------------------------- 1 | // Created by Helge Heß on 12.06.23. 2 | 3 | import SwiftSyntax 4 | 5 | protocol StaticStringEvaluation { 6 | 7 | func evaluateAsString() throws -> String 8 | } 9 | 10 | extension ExprSyntax: StaticStringEvaluation { 11 | 12 | func evaluateAsString() throws -> String { 13 | // TBD: Is there a better way to do this matching? Using a visitor maybe? 14 | if let typed = self.as(StringLiteralExprSyntax.self) { 15 | return try typed.evaluateAsString() 16 | } 17 | if let typed = self.as(IntegerLiteralExprSyntax.self) { 18 | return try typed.evaluateAsString() 19 | } 20 | if let typed = self.as(SequenceExprSyntax.self) { 21 | return try typed.evaluateAsString() 22 | } 23 | if let typed = self.as(ExprListSyntax.self) { 24 | return try typed.evaluateAsString() 25 | } 26 | if let typed = self.as(TupleExprSyntax.self) { 27 | return try typed.evaluateAsString() 28 | } 29 | 30 | throw MacroDiagnostic.unsupportedExpression 31 | } 32 | } 33 | 34 | extension StringLiteralExprSyntax: StaticStringEvaluation { 35 | 36 | func evaluateAsString() throws -> String { 37 | guard let value = representedLiteralValue else { 38 | throw MacroDiagnostic.couldNotGetStringValue 39 | } 40 | return value 41 | } 42 | } 43 | 44 | extension IntegerLiteralExprSyntax: StaticStringEvaluation { 45 | 46 | func evaluateAsString() throws -> String { digits.text } 47 | } 48 | 49 | extension SequenceExprSyntax: StaticStringEvaluation { 50 | 51 | func evaluateAsString() throws -> String { try elements.evaluateAsString() } 52 | } 53 | 54 | extension ExprListSyntax: StaticStringEvaluation { 55 | 56 | func evaluateAsString() throws -> String { 57 | guard let firstExpr = first else { // empty 58 | throw MacroDiagnostic.couldNotGetStringValue 59 | } 60 | 61 | // It must start w/ something that can result in a String 62 | var string = try firstExpr.evaluateAsString() 63 | 64 | for expr in dropFirst() { 65 | if let op = expr.as(BinaryOperatorExprSyntax.self) { 66 | guard case .binaryOperator(let op) = op.operatorToken.tokenKind else { 67 | throw MacroDiagnostic.unsupportedExpression 68 | } 69 | guard op == "+" else { 70 | throw MacroDiagnostic.unsupportedExpression 71 | } 72 | } 73 | else { 74 | // This allows `"19" "89"`, w/o an operator :-) 75 | string += try expr.evaluateAsString() 76 | } 77 | } 78 | 79 | return string 80 | } 81 | } 82 | 83 | extension TupleExprSyntax: StaticStringEvaluation { 84 | 85 | func evaluateAsString() throws -> String { 86 | try elementList.evaluateAsString() 87 | } 88 | } 89 | 90 | extension TupleExprElementListSyntax: StaticStringEvaluation { 91 | 92 | func evaluateAsString() throws -> String { 93 | try reduce("") { last, element in 94 | last + (try element.expression.evaluateAsString()) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/MacroCowsTests/MacroCowsTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftSyntaxMacrosTestSupport 3 | import XCTest 4 | @testable import MacroCowsMacros 5 | import cows 6 | import Foundation 7 | 8 | let testMacros: [String: Macro.Type] = [ 9 | "🐮": MacroCow.self 10 | ] 11 | 12 | final class MacroCowsTests: XCTestCase { 13 | 14 | func testStringLiteral() { 15 | assertMacroExpansion( 16 | """ 17 | #🐮("1989") 18 | """, 19 | expandedSource: 20 | "#\" (__) (__)\\#n (oo) (oo)\\#n ______________\\/______ ____ /-------\\/ __\\#n /___/___/___/___/___/_/| /___/| / | || _/_/|\\#n |___|___|___|___|___|_|| |__||/|* //-----|||_|_||\\#n |_|___|___|___|___|___|| |_|__|/|^^ ^/|___||\\#n |___|___|___|___|___|_|/ |___|_|/ |___|_|/\\#n Cow in the GDR before... ...and after 9-Nov-1989\"#", 21 | macros: testMacros 22 | ) 23 | } 24 | 25 | func testIntegerArgument() { 26 | assertMacroExpansion( 27 | """ 28 | #🐮(1989) 29 | """, 30 | expandedSource: 31 | "#\" (__) (__)\\#n (oo) (oo)\\#n ______________\\/______ ____ /-------\\/ __\\#n /___/___/___/___/___/_/| /___/| / | || _/_/|\\#n |___|___|___|___|___|_|| |__||/|* //-----|||_|_||\\#n |_|___|___|___|___|___|| |_|__|/|^^ ^/|___||\\#n |___|___|___|___|___|_|/ |___|_|/ |___|_|/\\#n Cow in the GDR before... ...and after 9-Nov-1989\"#", 32 | macros: testMacros 33 | ) 34 | } 35 | 36 | func testBoolArgument() { 37 | assertMacroExpansion( 38 | """ 39 | #🐮(false) 40 | """, 41 | expandedSource: "\"\"", 42 | diagnostics: [ 43 | .init(message: "Not a supported expression type, use literal Strings.", 44 | line: 1, column: 1, 45 | severity: .error) 46 | ], 47 | macros: testMacros 48 | ) 49 | } 50 | 51 | func testStaticStringAddition() { 52 | assertMacroExpansion( 53 | """ 54 | #🐮(("19" + ("89"))) 55 | """, 56 | expandedSource: 57 | "#\" (__) (__)\\#n (oo) (oo)\\#n ______________\\/______ ____ /-------\\/ __\\#n /___/___/___/___/___/_/| /___/| / | || _/_/|\\#n |___|___|___|___|___|_|| |__||/|* //-----|||_|_||\\#n |_|___|___|___|___|___|| |_|__|/|^^ ^/|___||\\#n |___|___|___|___|___|_|/ |___|_|/ |___|_|/\\#n Cow in the GDR before... ...and after 9-Nov-1989\"#", 58 | macros: testMacros 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /images/MacroCows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helje5/MacroCows/674301764cea8733f99f059c8a9521bfcabc3ed8/images/MacroCows.png --------------------------------------------------------------------------------