Skip to content

Commit

Permalink
Add Hashable conformance to generated type (#110)
Browse files Browse the repository at this point in the history
* Extract String(describing: key) helper into computed property for reuse

* generate type with Hashable and Equatable conformance

* Support older versions of Swift Syntax

* Bump minimum version of swift-snapshot-testing to 1.17.0

* Remove redundant Equatable type from inheretence clause
  • Loading branch information
liamnichols authored Jul 28, 2024
1 parent 3bf9f3e commit 3167c64
Show file tree
Hide file tree
Showing 25 changed files with 439 additions and 69 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"),
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0" ..< "601.0.0-prerelease"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.13.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
],
targets: [
Expand Down
4 changes: 4 additions & 0 deletions Sources/StringGenerator/Extensions/TokenSyntax+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ extension TokenSyntax {
case LocalizedStringKey
case Text
case Sendable
case Hasher
case Bool
case Equatable
case Hashable
}

static func type(_ value: MetaType) -> TokenSyntax {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ extension SourceFile.StringExtension {
[keyProperty, argumentsProperty, tableProperty]
}

var comparableProperties: [Property] {
[_keyProperty, argumentsProperty, tableProperty]
}

let keyProperty = Property(name: "key", type: .identifier(.StaticString))

let argumentsProperty = Property(name: "arguments", type: ArrayTypeSyntax(element: .identifier(.Argument)))
Expand All @@ -46,6 +50,8 @@ extension SourceFile.StringExtension {

let bundleProperty = Property(name: "bundle", type: .identifier(.Bundle))

let _keyProperty = Property(name: "_key", type: .identifier(.String))

let defaultValueProperty = Property(
name: "defaultValue",
type: MemberTypeSyntax(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct StringStringsTableArgumentEnumSnippet: Snippet {
}

var inheritanceClause: InheritanceClauseSyntax? {
InheritanceClauseSyntax(.Sendable)
InheritanceClauseSyntax(.Hashable, .Sendable)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import SwiftSyntax
import SwiftSyntaxBuilder

struct StringStringsTableComparisonFunctionSnippet: Snippet {
let stringsTable: SourceFile.StringExtension.StringsTableStruct
let leftArg: TokenSyntax = "lhs"
let rightArg: TokenSyntax = "rhs"

var syntax: some DeclSyntaxProtocol {
FunctionDeclSyntax(
modifiers: modifiers,
name: .binaryOperator("=="),
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax {
FunctionParameterSyntax(
firstName: leftArg,
type: IdentifierTypeSyntax(name: stringsTable.type)
)
FunctionParameterSyntax(
firstName: rightArg,
type: IdentifierTypeSyntax(name: stringsTable.type)
)
},
returnClause: ReturnClauseSyntax(type: .identifier(.Bool))
),
body: CodeBlockSyntax(statements: body)
)
}

@DeclModifierListBuilder
var modifiers: DeclModifierListSyntax {
DeclModifierSyntax(name: stringsTable.accessLevel.token)
DeclModifierSyntax(name: .keyword(.static))
}

var body: CodeBlockItemListSyntax {
// Array of `lhs.foo == rhs.foo`
var comparisons = stringsTable.comparableProperties.map { property in
InfixOperatorExprSyntax(
leftOperand: MemberAccessExprSyntax(leftArg, property.name),
operator: BinaryOperatorExprSyntax(operator: .binaryOperator("==")),
rightOperand: MemberAccessExprSyntax(rightArg, property.name)
)
}

var next = comparisons.removeLast()
while !comparisons.isEmpty {
let left = comparisons.removeLast()

next = InfixOperatorExprSyntax(
leftOperand: left,
operator: BinaryOperatorExprSyntax(operator: .binaryOperator("&&")),
rightOperand: next
)
}

return [CodeBlockItemSyntax(item: .expr(ExprSyntax(next)))]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SwiftSyntax
import SwiftSyntaxBuilder

struct StringStringsTableHashIntoFunctionSnippet: Snippet {
let stringsTable: SourceFile.StringExtension.StringsTableStruct
let hasherToken: TokenSyntax = "hasher"

var syntax: some DeclSyntaxProtocol {
FunctionDeclSyntax(
modifiers: modifiers,
name: "hash",
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax {
FunctionParameterSyntax(
firstName: "into",
secondName: hasherToken,
type: hasherType
)
}
),
body: CodeBlockSyntax(statements: body)
)
}

var hasherType: AttributedTypeSyntax {
#if canImport(SwiftSyntax600)
AttributedTypeSyntax(
specifiers: TypeSpecifierListSyntax {
SimpleTypeSpecifierSyntax(specifier: .keyword(.inout))
},
baseType: IdentifierTypeSyntax(name: .type(.Hasher))
)
#else
AttributedTypeSyntax(
specifier: .keyword(.inout),
baseType: IdentifierTypeSyntax(name: .type(.Hasher))
)
#endif
}

@DeclModifierListBuilder
var modifiers: DeclModifierListSyntax {
DeclModifierSyntax(name: stringsTable.accessLevel.token)
}

@CodeBlockItemListBuilder
var body: CodeBlockItemListSyntax {
for property in stringsTable.comparableProperties {
FunctionCallExprSyntax(callee: MemberAccessExprSyntax(hasherToken, "combine")) {
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: property.name))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct StringStringsTableStructSnippet: Snippet {
}

var inheritanceClause: InheritanceClauseSyntax? {
InheritanceClauseSyntax(.Sendable)
InheritanceClauseSyntax(.Hashable, .Sendable)
}

var memberBlock: MemberBlockSyntax {
Expand Down Expand Up @@ -105,6 +105,27 @@ struct StringStringsTableStructSnippet: Snippet {
StringStringsTableDefaultValueComputedPropertySnippet(
stringsTable: stringsTable
)
.syntax
.with(\.trailingTrivia, .newlines(2))

// fileprivate var _key: String { String(describing: key) }
StringStringsTableUnderscoredKeyComputedPropertySnippet(
stringsTable: stringsTable
)
.syntax
.with(\.trailingTrivia, .newlines(2))

// func hash(into hasher: Hasher) { ... }
StringStringsTableHashIntoFunctionSnippet(
stringsTable: stringsTable
)
.syntax
.with(\.trailingTrivia, .newlines(2))

// static func ==(lhs: Localizable, rhs: Localizable) -> Bool { ... }
StringStringsTableComparisonFunctionSnippet(
stringsTable: stringsTable
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import SwiftSyntax
import SwiftSyntaxBuilder

struct StringStringsTableUnderscoredKeyComputedPropertySnippet: Snippet {
let stringsTable: SourceFile.StringExtension.StringsTableStruct

var syntax: some DeclSyntaxProtocol {
// fileprivate var _key: String { ... }
VariableDeclSyntax(
modifiers: modifiers,
bindingSpecifier: .keyword(.var)
) {
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: stringsTable._keyProperty.name),
typeAnnotation: typeAnnotation,
accessorBlock: AccessorBlockSyntax(accessors: .getter(body))
)
}
}

@DeclModifierListBuilder
var modifiers: DeclModifierListSyntax {
DeclModifierSyntax(name: .keyword(.fileprivate))
}

var typeAnnotation: TypeAnnotationSyntax {
TypeAnnotationSyntax(
type: stringsTable._keyProperty.type
)
}

@CodeBlockItemListBuilder
var body: CodeBlockItemListSyntax {
// String(describing: key)
FunctionCallExprSyntax(
callee: DeclReferenceExprSyntax(baseName: .type(.String))
) {
LabeledExprSyntax(
label: "describing",
expression: DeclReferenceExprSyntax(baseName: stringsTable.keyProperty.name)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ struct LocalizedStringKeyOverrideKeySnippet: Snippet {
///
/// This method allows you to change the key after initialization in order
/// to match the value that might be defined in the strings table.
fileprivate mutating func overrideKeyForLookup(using key: StaticString) {
fileprivate mutating func overrideKeyForLookup(using key: String) {
withUnsafeMutablePointer(to: &self) { pointer in
let raw = UnsafeMutableRawPointer(pointer)
let bound = raw.assumingMemoryBound(to: String.self)
bound.pointee = String(describing: key)
bound.pointee = key
}
}
""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ extension TextInitializerSnippet: Snippet {
) {
LabeledExprSyntax(
label: "using",
expression: MemberAccessExprSyntax(variableToken, stringsTable.keyProperty.name)
expression: MemberAccessExprSyntax(variableToken, stringsTable._keyProperty.name)
)
}
.with(\.trailingTrivia, .newlines(2))
Expand Down
2 changes: 1 addition & 1 deletion Sources/StringGenerator/StringGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private extension String {
// https://github.com/liamnichols/xcstrings-tool/issues/97
func patchingSwift6CompatibilityIssuesIfNeeded() -> String {
#if !canImport(SwiftSyntax600)
replacing(/[#@]available\s\(/, with: { match in
replacing(/(?:[#@]available|==)\s\(/, with: { match in
match.output.filter { !$0.isWhitespace }
})
#else
Expand Down
10 changes: 10 additions & 0 deletions Tests/PluginTests/PluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,14 @@ final class PluginTests: XCTestCase {
"Hello John, I have 1 plural"
)
}

func testComparisons() {
let foo1 = String.Localizable.demoBasic
let foo2 = String.Localizable.demoBasic
XCTAssertEqual(foo1, foo2)

let bar1 = String.Localizable.multiline(1)
let bar2 = String.Localizable.multiline(2)
XCTAssertNotEqual(bar1, bar2)
}
}
8 changes: 6 additions & 2 deletions Tests/XCStringsToolTests/GenerateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import SnapshotTesting
import XCTest

final class GenerateTests: FixtureTestCase {
override func invokeTest() {
withSnapshotTesting(record: .missing) {
super.invokeTest()
}
}

func testGenerate() throws {
try eachFixture { inputURL in
if !inputURL.lastPathComponent.hasPrefix("!") {
Expand Down Expand Up @@ -78,7 +84,6 @@ private extension GenerateTests {
func snapshot(
for inputURLs: URL...,
accessLevel: String? = nil,
record: Bool = false,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
Expand All @@ -87,7 +92,6 @@ private extension GenerateTests {
of: try run(for: inputURLs, accessLevel: accessLevel),
as: .sourceCode,
named: inputURLs.first!.stem,
record: record,
file: file,
testName: testName,
line: line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ extension String {
/// ```
///
/// - SeeAlso: [XCStrings Tool Documentation - Using the generated source code](https://swiftpackageindex.com/liamnichols/xcstrings-tool/0.5.2/documentation/documentation/using-the-generated-source-code)
internal struct FormatSpecifiers: Sendable {
internal struct FormatSpecifiers: Hashable, Sendable {
#if !SWIFT_PACKAGE
private class BundleLocator {
}
#endif

enum Argument: Sendable {
enum Argument: Hashable, Sendable {
case int(Int)
case uint(UInt)
case float(Float)
Expand Down Expand Up @@ -331,6 +331,20 @@ extension String {
let makeDefaultValue = String.LocalizationValue.init(stringInterpolation:)
return makeDefaultValue(stringInterpolation)
}

fileprivate var _key: String {
String(describing: key)
}

internal func hash(into hasher: inout Hasher) {
hasher.combine(_key)
hasher.combine(arguments)
hasher.combine(table)
}

internal static func ==(lhs: FormatSpecifiers, rhs: FormatSpecifiers) -> Bool {
lhs._key == rhs._key && lhs.arguments == rhs.arguments && lhs.table == rhs.table
}
}

internal init(formatSpecifiers: FormatSpecifiers, locale: Locale? = nil) {
Expand Down Expand Up @@ -390,7 +404,7 @@ extension Text {
let makeKey = LocalizedStringKey.init(stringInterpolation:)

var key = makeKey(stringInterpolation)
key.overrideKeyForLookup(using: formatSpecifiers.key)
key.overrideKeyForLookup(using: formatSpecifiers._key)

self.init(key, tableName: formatSpecifiers.table, bundle: formatSpecifiers.bundle)
}
Expand Down Expand Up @@ -427,11 +441,11 @@ extension LocalizedStringKey {
///
/// This method allows you to change the key after initialization in order
/// to match the value that might be defined in the strings table.
fileprivate mutating func overrideKeyForLookup(using key: StaticString) {
fileprivate mutating func overrideKeyForLookup(using key: String) {
withUnsafeMutablePointer(to: &self) { pointer in
let raw = UnsafeMutableRawPointer(pointer)
let bound = raw.assumingMemoryBound(to: String.self)
bound.pointee = String(describing: key)
bound.pointee = key
}
}
}
Expand Down
Loading

0 comments on commit 3167c64

Please sign in to comment.