{"id":13496756,"url":"https://github.com/stackotter/swift-macro-toolkit","last_synced_at":"2025-05-16T14:07:35.997Z","repository":{"id":176326597,"uuid":"654845758","full_name":"stackotter/swift-macro-toolkit","owner":"stackotter","description":"A powerful toolkit for creating concise and expressive Swift macros","archived":false,"fork":false,"pushed_at":"2025-03-13T14:42:53.000Z","size":119,"stargazers_count":259,"open_issues_count":6,"forks_count":15,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-12T11:55:36.849Z","etag":null,"topics":["hacktoberfest"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stackotter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"stackotter"}},"created_at":"2023-06-17T05:46:07.000Z","updated_at":"2025-04-08T14:31:50.000Z","dependencies_parsed_at":"2024-02-09T14:54:32.572Z","dependency_job_id":"447429c2-a120-42fc-ba54-b00fc0609c36","html_url":"https://github.com/stackotter/swift-macro-toolkit","commit_stats":{"total_commits":57,"total_committers":12,"mean_commits":4.75,"dds":0.2807017543859649,"last_synced_commit":"fe6653d8e85d398c831b252ac9e0124304dc8782"},"previous_names":["stackotter/swift-macro-toolkit"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackotter%2Fswift-macro-toolkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackotter%2Fswift-macro-toolkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackotter%2Fswift-macro-toolkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackotter%2Fswift-macro-toolkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stackotter","download_url":"https://codeload.github.com/stackotter/swift-macro-toolkit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254544146,"owners_count":22088807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest"],"created_at":"2024-07-31T19:01:58.853Z","updated_at":"2025-05-16T14:07:35.979Z","avatar_url":"https://github.com/stackotter.png","language":"Swift","funding_links":["https://github.com/sponsors/stackotter"],"categories":["Swift"],"sub_categories":[],"readme":"# Swift Macro Toolkit\n\n**Did you know that `-0xF_ep-0_2` is a valid floating point literal in Swift?** Well you probably didn't\n(it's equal to -63.5), and as a macro author you shouldn't even have to care! Among many things, Macro\nToolkit shields you from edge cases so that users can use your macros in whatever weird (but correct)\nmanners they may desire.\n\n**You don't need in-depth knowledge of Swift's syntax to make a robust macro, you just need an idea.**\n\n## Supporting Swift Macro Toolkit\n\nIf you find Swift Macro Toolkit useful, please consider supporting me by\n[becoming a sponsor](https://github.com/sponsors/stackotter). I spend most of my spare time\nworking on open-source projects, and each sponsorship helps me focus more time on making\nhigh quality tools and libraries for the community. \n\n## Why use it?\n\nSee for yourself;\n\n### Get the value of a float literal\n\nDoes `-0xF_ep-0_2` look like the type of floating point literal you want to implement parsing for?\nNope; but you don't have to.\n\n#### With Macro Toolkit\n\n```swift\nreturn literal.value\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eWithout Macro Toolkit (worth a look)\u003c/summary\u003e\n\n  ```swift\n  let string = _syntax.floatingDigits.text\n\n  let isHexadecimal: Bool\n  let stringWithoutPrefix: String\n  switch string.prefix(2) {\n      case \"0x\":\n          isHexadecimal = true\n          stringWithoutPrefix = String(string.dropFirst(2))\n      default:\n          isHexadecimal = false\n          stringWithoutPrefix = string\n  }\n\n  let exponentSeparator: Character = isHexadecimal ? \"p\" : \"e\"\n  let parts = stringWithoutPrefix.lowercased().split(separator: exponentSeparator)\n  guard parts.count \u003c= 2 else {\n      fatalError(\"Float literal cannot contain more than one exponent separator\")\n  }\n\n  let exponentValue: Int\n  if parts.count == 2 {\n      // The exponent part is always decimal\n      let exponentPart = parts[1]\n      let exponentPartWithoutUnderscores = exponentPart.replacingOccurrences(of: \"_\", with: \"\")\n      guard\n          exponentPart.first != \"_\",\n          !exponentPart.starts(with: \"-_\"),\n          let exponent = Int(exponentPartWithoutUnderscores)\n      else {\n          fatalError(\"Float literal has invalid exponent part: \\(string)\")\n      }\n      exponentValue = exponent\n  } else {\n      exponentValue = 0\n  }\n\n  let partsBeforeExponent = parts[0].split(separator: \".\")\n  guard partsBeforeExponent.count \u003c= 2 else {\n      fatalError(\"Float literal cannot contain more than one decimal point: \\(string)\")\n  }\n\n  // The integer part can contain underscores anywhere except for the first character (which must be a digit).\n  let radix = isHexadecimal ? 16 : 10\n  let integerPart = partsBeforeExponent[0]\n  let integerPartWithoutUnderscores = integerPart.replacingOccurrences(of: \"_\", with: \"\")\n  guard\n      integerPart.first != \"_\",\n      let integerPartValue = Int(integerPartWithoutUnderscores, radix: radix).map(Double.init)\n  else {\n      fatalError(\"Float literal has invalid integer part: \\(string)\")\n  }\n\n  let fractionalPartValue: Double\n  if partsBeforeExponent.count == 2 {\n      // The fractional part can contain underscores anywhere except for the first character (which must be a digit).\n      let fractionalPart = partsBeforeExponent[1]\n      let fractionalPartWithoutUnderscores = fractionalPart.replacingOccurrences(of: \"_\", with: \"\")\n      guard\n          fractionalPart.first != \"_\",\n          let fractionalPartDigitsValue = Int(fractionalPartWithoutUnderscores, radix: radix)\n      else {\n          fatalError(\"Float literal has invalid fractional part: \\(string)\")\n      }\n\n      fractionalPartValue = Double(fractionalPartDigitsValue) / pow(Double(radix), Double(fractionalPart.count - 1))\n  } else {\n      fractionalPartValue = 0\n  }\n\n  let base: Double = isHexadecimal ? 2 : 10\n  let multiplier = pow(base, Double(exponentValue))\n  let sign: Double = _negationSyntax == nil ? 1 : -1\n\n  return (integerPartValue + fractionalPartValue) * multiplier * sign\n  ```\n\u003c/details\u003e\n\n### Type destructuring\n\n#### With Macro Toolkit\n\n```swift\nguard case let .simple(\"Result\", (successType, failureType))? = destructure(returnType) else {\n    throw MacroError(\"Invalid return type\")\n}\n```\n\n#### Without Macro Toolkit\n\n```swift\nguard\n    let simpleReturnType = returnType.as(SimpleTypeIdentifierSyntax.self),\n    simpleReturnType.name.description == \"Result\",\n    let genericArguments = (simpleReturnType.genericArgumentClause?.arguments).map(Array.init),\n    genericArguments.count == 2\nelse {\n    throw MacroError(\"Invalid return type\")\n}\nlet successType = genericArguments[0]\nlet failureType = genericArguments[1]\n```\n\n### Type normalization\n\nSwift has many different ways to express a single type. To name a few such cases; `() == Void`,\n`Int? == Optional\u003cInt\u003e`, and `[Int] == Array\u003cInt\u003e`. Swift Macro Toolkit strives to hide these details\nfrom you, so you don't have to handle all the edge cases.\n\n#### With Macro Toolkit\n\n```swift\nfunction.returnsVoid\n```\n\n#### Without Macro Toolkit\n\n```swift\nfunc returnsVoid(_ function: FunctionDeclSyntax) -\u003e Bool {\n    // Function can either have no return type annotation, `()`, `Void`, or a nested single\n    // element tuple with a Void-like inner type (e.g. `((((()))))` or `(((((Void)))))`)\n    func isVoid(_ type: TypeSyntax) -\u003e Bool {\n        if type.description == \"Void\" || type.description == \"()\" {\n            return true\n        }\n\n        guard let tuple = type.as(TupleTypeSyntax.self) else {\n            return false\n        }\n\n        if let element = tuple.elements.first, tuple.elements.count == 1 {\n            let isUnlabeled = element.name == nil \u0026\u0026 element.secondName == nil\n            return isUnlabeled \u0026\u0026 isVoid(TypeSyntax(element.type))\n        }\n        return false\n    }\n\n    guard let returnType = function.output?.returnType else {\n        return false\n    }\n    return isVoid(returnType)\n}\n```\n\n### Get the value of a string literal\n\nGetting the value of a string literal (without interpolations) can be incredibly\ntedious if you want to do it the right way. You have to evaluate all escape sequences\nyourself (unicode ones are particularly annoying e.g. `\\u{2020}`). And then if a user\nwants to use a raw string literal (e.g. `#\"This isn't a newline \\n\"#`), things get even\nmore difficult to get right. Don't fear though, Swift Macro Toolkit has you covered.\n\n#### With Macro Toolkit\n\n```swift\nreturn literal.value\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eWithout Macro Toolkit\u003c/summary\u003e\n\n  ```swift\n  let segments = _syntax.segments.compactMap { (segment) -\u003e String? in\n      guard case let .stringSegment(segment) = segment else {\n          return nil\n      }\n      return segment.content.text\n  }\n  guard segments.count == _syntax.segments.count else {\n      return nil\n  }\n\n  let map: [Character: Character] = [\n      \"\\\\\": \"\\\\\",\n      \"n\": \"\\n\",\n      \"r\": \"\\r\",\n      \"t\": \"\\t\",\n      \"0\": \"\\0\",\n      \"\\\"\": \"\\\"\",\n      \"'\": \"'\"\n  ]\n  let hexadecimalCharacters = \"0123456789abcdefABCDEF\"\n\n  // The length of the `\\###...` sequence that starts an escape sequence (zero hashes if not a raw string)\n  let escapeSequenceDelimiterLength = (_syntax.openDelimiter?.text.count ?? 0) + 1\n  // Evaluate backslash escape sequences within each segment before joining them together\n  let transformedSegments = segments.map { segment in\n      var characters: [Character] = []\n      var inEscapeSequence = false\n      var iterator = segment.makeIterator()\n      var escapeSequenceDelimiterPosition = 0 // Tracks the current position in the delimiter if parsing one\n      while let c = iterator.next() {\n          if inEscapeSequence {\n              if let replacement = map[c] {\n                  characters.append(replacement)\n              } else if c == \"u\" {\n                  var count = 0\n                  var digits: [Character] = []\n                  var iteratorCopy = iterator\n\n                  guard iterator.next() == \"{\" else {\n                      fatalError(\"Expected '{' in unicode scalar escape sequence\")\n                  }\n\n                  var foundClosingBrace = false\n                  while let c = iterator.next() {\n                      if c == \"}\" {\n                          foundClosingBrace = true\n                          break\n                      }\n\n                      guard hexadecimalCharacters.contains(c) else {\n                          iterator = iteratorCopy\n                          break\n                      }\n                      iteratorCopy = iterator\n\n                      digits.append(c)\n                      count += 1\n                  }\n\n                  guard foundClosingBrace else {\n                      fatalError(\"Expected '}' in unicode scalar escape sequence\")\n                  }\n\n                  if !(1...8).contains(count) {\n                      fatalError(\"Invalid unicode character escape sequence (must be 1 to 8 digits)\")\n                  }\n\n                  guard\n                      let value = UInt32(digits.map(String.init).joined(separator: \"\"), radix: 16),\n                      let scalar = Unicode.Scalar(value)\n                  else {\n                      fatalError(\"Invalid unicode scalar hexadecimal value literal\")\n                  }\n\n                  characters.append(Character(scalar))\n              }\n              inEscapeSequence = false\n          } else if c == \"\\\\\" \u0026\u0026 escapeSequenceDelimiterPosition == 0 {\n              escapeSequenceDelimiterPosition += 1\n          } else if !inEscapeSequence \u0026\u0026 c == \"#\" \u0026\u0026 escapeSequenceDelimiterPosition != 0 {\n              escapeSequenceDelimiterPosition += 1\n          } else {\n              if escapeSequenceDelimiterPosition != 0 {\n                  characters.append(\"\\\\\")\n                  for _ in 0..\u003c(escapeSequenceDelimiterPosition - 1) {\n                      characters.append(\"#\")\n                  }\n                  escapeSequenceDelimiterPosition = 0\n              }\n              characters.append(c)\n          }\n          if escapeSequenceDelimiterPosition == escapeSequenceDelimiterLength {\n              inEscapeSequence = true\n              escapeSequenceDelimiterPosition = 0\n          }\n      }\n      return characters.map(String.init).joined(separator: \"\")\n  }\n\n  return transformedSegments.joined(separator: \"\")\n  ```\n\u003c/details\u003e\n\n### Diagnostic creation\n\n#### With Macro Toolkit\n\n```swift\nlet diagnostic = DiagnosticBuilder(for: function._syntax.funcKeyword)\n    .message(\"can only add a completion-handler variant to an 'async' function\")\n    .messageID(domain: \"AddCompletionHandlerMacro\", id: \"MissingAsync\")\n    .suggestReplacement(\n        \"add 'async'\",\n        old: function._syntax.signature,\n        new: newSignature\n    )\n    .build()\n```\n\n#### Without Macro Toolkit\n\n```swift\nlet diagnostic = Diagnostic(\n    node: Syntax(funcDecl.funcKeyword),\n    message: SimpleDiagnosticMessage(\n        message: \"can only add a completion-handler variant to an 'async' function\",\n        diagnosticID: MessageID(domain: \"AddCompletionHandlerMacro\", id: \"MissingAsync\"),\n        severity: .error\n    ),\n    fixIts: [\n        FixIt(\n            message: SimpleDiagnosticMessage(\n                message: \"add 'async'\",\n                diagnosticID: MessageID(domain: \"AddCompletionHandlerMacro\", id: \"MissingAsync\"),\n                severity: .error\n            ),\n            changes: [\n                FixIt.Change.replace(\n                    oldNode: Syntax(funcDecl.signature),\n                    newNode: Syntax(newSignature)\n                )\n            ]\n        ),\n    ]\n)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstackotter%2Fswift-macro-toolkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstackotter%2Fswift-macro-toolkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstackotter%2Fswift-macro-toolkit/lists"}