-
Notifications
You must be signed in to change notification settings - Fork 549
Implement thread safety macros #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: swift-6
Are you sure you want to change the base?
Changes from 3 commits
fa8b941
2aee7c4
9005081
63e37ee
df60493
4b66098
26d41d7
97c641b
585c8b3
fb2c84f
4eabf1f
2f104a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // For licensing see accompanying LICENSE.md file. | ||
| // Copyright © 2026 Argmax, Inc. All rights reserved. | ||
|
|
||
| import SwiftCompilerPlugin | ||
| import SwiftSyntaxMacros | ||
|
|
||
| @main | ||
| struct ArgmaxCoreMacroPlugin: CompilerPlugin { | ||
| let providingMacros: [Macro.Type] = [ | ||
| ThreadSafeMacro.self, | ||
| ThreadSafeInitializerMacro.self, | ||
| ThreadSafePropertyMacro.self, | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // For licensing see accompanying LICENSE.md file. | ||
| // Copyright © 2026 Argmax, Inc. All rights reserved. | ||
|
|
||
| import Foundation | ||
| import SwiftCompilerPlugin | ||
| import SwiftDiagnostics | ||
| import SwiftSyntax | ||
| import SwiftSyntaxBuilder | ||
| import SwiftSyntaxMacros | ||
| import RegexBuilder | ||
|
|
||
| extension ClassDeclSyntax { | ||
| private static let trailingCallSuffixRegex = Regex { | ||
| "(" | ||
| ZeroOrMore { | ||
| CharacterClass.anyOf(")").inverted | ||
| } | ||
| ")" | ||
| Anchor.endOfLine | ||
| } | ||
|
|
||
| private static let integerLiteralRegex = Regex { | ||
| Anchor.startOfLine | ||
| Optionally { "-" } | ||
| OneOrMore(.digit) | ||
| Anchor.endOfLine | ||
| } | ||
|
|
||
| private static let doubleLiteralRegex = Regex { | ||
| Anchor.startOfLine | ||
| Optionally { "-" } | ||
| OneOrMore(.digit) | ||
| Optionally { "." } | ||
| ZeroOrMore(.digit) | ||
| Anchor.endOfLine | ||
| } | ||
|
|
||
| private static let quotedStringRegex = Regex { | ||
| Anchor.startOfLine | ||
| "\"" | ||
| ZeroOrMore { | ||
| CharacterClass.anyOf("\n").inverted | ||
| } | ||
| "\"" | ||
| Anchor.endOfLine | ||
| } | ||
|
|
||
| /// Returns the list of mutable stored properties in the class. | ||
| var storedVariables: [(name: String, type: String, defaultValue: String?)] { | ||
| var storedVars = [(String, String, String?)]() | ||
|
|
||
| for member in memberBlock.members { | ||
| guard | ||
| let varDecl = member.decl.as(VariableDeclSyntax.self), | ||
| varDecl.isMutable | ||
| else { continue } | ||
|
|
||
| for binding in varDecl.bindings { | ||
| if | ||
| binding.accessorBlock == nil, | ||
| let pattern = binding.pattern.as(IdentifierPatternSyntax.self) | ||
| { | ||
| let name = pattern.identifier.text.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let defaultValue = binding.initializer?.value.trimmedDescription | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) ?? binding | ||
| .typeAnnotation?.type.defaultValueForOptional | ||
|
|
||
| if let typeAnnotation = binding.typeAnnotation { | ||
| let type = typeAnnotation.type.trimmedDescription.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| storedVars.append((name, type, defaultValue)) | ||
| } else if let defaultValue { | ||
| // Heuristically tries to infer the type from the default value | ||
| let value = stripTrailingCallSuffix(from: defaultValue) | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| let type: String = | ||
| if value == "true" || value == "false" { | ||
| "Bool" | ||
| } else if value.wholeMatch(of: Self.integerLiteralRegex) != nil { | ||
| "Int" | ||
| } else if value.wholeMatch(of: Self.doubleLiteralRegex) != nil { | ||
| "Double" | ||
| } else if value.wholeMatch(of: Self.quotedStringRegex) != nil { | ||
| "String" | ||
| } else { | ||
| value | ||
| } | ||
| storedVars.append((name, type, defaultValue)) | ||
|
Comment on lines
+71
to
+88
|
||
| } | ||
| } | ||
| } | ||
| } | ||
| return storedVars | ||
| } | ||
|
|
||
| private func stripTrailingCallSuffix(from value: String) -> String { | ||
| guard let match = value.firstMatch(of: Self.trailingCallSuffixRegex) else { | ||
| return value | ||
| } | ||
| return String(value[..<match.range.lowerBound]) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // For licensing see accompanying LICENSE.md file. | ||
| // Copyright © 2026 Argmax, Inc. All rights reserved. | ||
|
|
||
| import SwiftSyntax | ||
|
|
||
| extension TypeSyntax { | ||
| var defaultValueForOptional: String? { | ||
| if self.as(OptionalTypeSyntax.self) != nil { | ||
| return "nil" | ||
| } | ||
| return nil | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||
| // For licensing see accompanying LICENSE.md file. | ||||
| // Copyright © 2026 Argmax, Inc. All rights reserved. | ||||
|
|
||||
| import Foundation | ||||
| import SwiftSyntax | ||||
|
|
||||
| extension VariableDeclSyntax { | ||||
| var isMutable: Bool { | ||||
| guard | ||||
| bindingSpecifier.text == "var", | ||||
| attributes.isEmpty, | ||||
|
||||
| attributes.isEmpty, |
Copilot
AI
Mar 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VariableDeclSyntax.isMutable requires attributes.isEmpty, which means a stored property already annotated with @ThreadSafeProperty will be excluded from ClassDeclSyntax.storedVariables. As a result, @ThreadSafe can generate an _InternalState that omits those fields, and the accessor expansion will then reference missing members (compile-time error). Consider splitting this into (a) “is mutable stored identifier binding” and (b) “eligible for auto-annotation”, and ensure storedVariables includes vars that already have @ThreadSafeProperty even if they have attributes.
Uh oh!
There was an error while loading. Please reload this page.