├── .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 | 
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 | 
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
--------------------------------------------------------------------------------