Skip to content

Commit

Permalink
Add variable prefix factory
Browse files Browse the repository at this point in the history
  • Loading branch information
Matejkob committed Jun 14, 2023
1 parent 24520e2 commit 9f4bd40
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 118 deletions.
57 changes: 6 additions & 51 deletions Sources/SpyableMacro/Factories/SpyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftSyntax
import SwiftSyntaxBuilder

struct SpyFactory {
private let variablePrefixFactory = VariablePrefixFactory()
private let variablesImplementationFactory = VariablesImplementationFactory()
private let callsCountFactory = CallsCountFactory()
private let calledFactory = CalledFactory()
Expand All @@ -14,10 +15,10 @@ struct SpyFactory {
func classDeclaration(for protocolDeclaration: ProtocolDeclSyntax) -> ClassDeclSyntax {
let identifier = TokenSyntax.identifier(protocolDeclaration.identifier.text + "Spy")

let variablesDeclarations = protocolDeclaration.memberBlock.members
let variableDeclarations = protocolDeclaration.memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }

let functionsDeclarations = protocolDeclaration.memberBlock.members
let functionDeclarations = protocolDeclaration.memberBlock.members
.compactMap { $0.decl.as(FunctionDeclSyntax.self) }

return ClassDeclSyntax(
Expand All @@ -28,14 +29,14 @@ struct SpyFactory {
)
},
memberBlockBuilder: {
for variableDeclaration in variablesDeclarations {
for variableDeclaration in variableDeclarations {
variablesImplementationFactory.variablesDeclarations(
protocolVariableDeclaration: variableDeclaration
)
}

for functionDeclaration in functionsDeclarations {
let variablePrefix = spyPropertyDescription(for: functionDeclaration)
for functionDeclaration in functionDeclarations {
let variablePrefix = variablePrefixFactory.text(for: functionDeclaration)
let parameterList = functionDeclaration.signature.input.parameterList

callsCountFactory.variableDeclaration(variablePrefix: variablePrefix)
Expand Down Expand Up @@ -72,50 +73,4 @@ struct SpyFactory {
}
)
}

// MARK: - Helpers

func spyPropertyDescription(for functionDeclaration: FunctionDeclSyntax) -> String {
/*
Sourcery doesn't destingiush
```
func foo()
```
from
```
func foo() -> String
```

I think that the best way to deal with it would be generate basic nameing and
If the colision would happend then deal with it some how
Mayby I can use `(declaration.as(FunctionDeclSyntax.self))?.signature.input.description`
*/

var parts: [String] = []

parts.append(functionDeclaration.identifier.text)

let parameterList = functionDeclaration.signature.input.parameterList

if !parameterList.isEmpty {
parts.append("With")
}

let parameters = parameterList
.reduce([String]()) { partialResult, parameter in
var partialResult = partialResult
partialResult.append(parameter.firstName.text)

if let secondNameText = parameter.secondName?.text {
partialResult.append(secondNameText)
}
return partialResult
}
.filter { $0 != "_" }
.map { $0.prefix(1).uppercased() + $0.dropFirst() }

parts.append(contentsOf: parameters)

return parts.joined(separator: "")
}
}
41 changes: 41 additions & 0 deletions Sources/SpyableMacro/Factories/VariablePrefixFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftSyntax
import SwiftSyntaxBuilder

/// The `VariablePrefixFactory` struct is responsible for creating a unique textual representation
/// for a given function declaration. This representation can be used as a prefix when naming variables
/// associated with that function.
///
/// The factory constructs the representation by combining the function name with the first names of its parameters.
///
/// For example, given the function declaration:
/// ```swift
/// func display(text: String, color: Color)
/// ```
/// the `VariablePrefixFactory` generates the following text:
/// ```
/// displayTextColor
/// ```
/// It will capitalize the first letter of each parameter name and append it to the function name.
/// Please note that if a parameter is underscored (anonymous), it's ignored.
struct VariablePrefixFactory {
func text(for functionDeclaration: FunctionDeclSyntax) -> String {
var parts: [String] = [functionDeclaration.identifier.text]

let parameterList = functionDeclaration.signature.input.parameterList

let parameters = parameterList
.map { $0.firstName.text }
.filter { $0 != "_" }
.map { $0.capitalizingFirstLetter() }

parts.append(contentsOf: parameters)

return parts.joined()
}
}

extension String {
fileprivate func capitalizingFirstLetter() -> String {
return prefix(1).uppercased() + dropFirst()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ struct VariablesImplementationFactory {
private let accessorRemovalVisitor = AccessorRemovalVisitor()

@MemberDeclListBuilder
func variablesDeclarations(protocolVariableDeclaration: VariableDeclSyntax) -> MemberDeclListSyntax {
func variablesDeclarations(
protocolVariableDeclaration: VariableDeclSyntax
) -> MemberDeclListSyntax {
if let binding = protocolVariableDeclaration.bindings.first {
if let variableType = binding.typeAnnotation?.type, variableType.is(OptionalTypeSyntax.self) {
accessorRemovalVisitor.visit(protocolVariableDeclaration)
Expand All @@ -15,10 +17,12 @@ struct VariablesImplementationFactory {
underlyingVariableDeclaration(binding: binding)
}
}
// TODO: Thing about throwing diagnostic here
// TODO: Consider throwing diagnostic warning/error here
}

private func protocolVariableDeclarationWithGetterAndSetter(binding: PatternBindingListSyntax.Element) -> VariableDeclSyntax {
private func protocolVariableDeclarationWithGetterAndSetter(
binding: PatternBindingListSyntax.Element
) -> VariableDeclSyntax {
let underlyingVariableName = underlyingVariableName(binding: binding)

return VariableDeclSyntax(
Expand All @@ -41,7 +45,9 @@ struct VariablesImplementationFactory {
)
}

private func underlyingVariableDeclaration(binding: PatternBindingListSyntax.Element) -> VariableDeclSyntax {
private func underlyingVariableDeclaration(
binding: PatternBindingListSyntax.Element
) -> VariableDeclSyntax {
VariableDeclSyntax(
bindingKeyword: .keyword(.var),
bindingsBuilder: {
Expand All @@ -56,7 +62,7 @@ struct VariablesImplementationFactory {
if let type = binding.typeAnnotation?.type {
TupleTypeElementSyntax(type: type)
}
// TODO: Thing about throwing diagnostic here
// Consider throwing diagnostic warning/error here
}
)
)
Expand All @@ -68,7 +74,7 @@ struct VariablesImplementationFactory {

private func underlyingVariableName(binding: PatternBindingListSyntax.Element) -> String {
guard let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
return "" // TODO: Thins about throwing diagnostic here
return "" // TODO: Consider throwing diagnostic warning/error here
}
let identifierText = identifierPattern.identifier.text

Expand Down
72 changes: 36 additions & 36 deletions Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,18 @@ final class UT_SpyFactory: XCTestCase {
result,
"""
class ViewModelProtocolSpy: ViewModelProtocol {
var fooWithTextCountCallsCount = 0
var fooWithTextCountCalled: Bool {
return fooWithTextCountCallsCount > 0
var fooTextCountCallsCount = 0
var fooTextCountCalled: Bool {
return fooTextCountCallsCount > 0
}
var fooWithTextCountReceivedArguments: (text: String, count: Int)?
var fooWithTextCountReceivedInvocations: [(text: String, count: Int)] = []
var fooWithTextCountClosure: ((String, Int) -> Void)?
var fooTextCountReceivedArguments: (text: String, count: Int)?
var fooTextCountReceivedInvocations: [(text: String, count: Int)] = []
var fooTextCountClosure: ((String, Int) -> Void)?
func foo(text: String, count: Int) {
fooWithTextCountCallsCount += 1
fooWithTextCountReceivedArguments = (text, count)
fooWithTextCountReceivedInvocations.append((text, count))
fooWithTextCountClosure?(text, count)
fooTextCountCallsCount += 1
fooTextCountReceivedArguments = (text, count)
fooTextCountReceivedInvocations.append((text, count))
fooTextCountClosure?(text, count)
}
}
"""
Expand Down Expand Up @@ -139,22 +139,22 @@ final class UT_SpyFactory: XCTestCase {
result,
"""
class ServiceProtocolSpy: ServiceProtocol {
var fooWithTextCountCallsCount = 0
var fooWithTextCountCalled: Bool {
return fooWithTextCountCallsCount > 0
var fooTextCountCallsCount = 0
var fooTextCountCalled: Bool {
return fooTextCountCallsCount > 0
}
var fooWithTextCountReceivedArguments: (text: String, count: Int)?
var fooWithTextCountReceivedInvocations: [(text: String, count: Int)] = []
var fooWithTextCountReturnValue: Decimal!
var fooWithTextCountClosure: ((String, Int) async -> Decimal)?
var fooTextCountReceivedArguments: (text: String, count: Int)?
var fooTextCountReceivedInvocations: [(text: String, count: Int)] = []
var fooTextCountReturnValue: Decimal!
var fooTextCountClosure: ((String, Int) async -> Decimal)?
func foo(text: String, count: Int) async -> Decimal {
fooWithTextCountCallsCount += 1
fooWithTextCountReceivedArguments = (text, count)
fooWithTextCountReceivedInvocations.append((text, count))
if fooWithTextCountClosure != nil {
return await fooWithTextCountClosure!(text, count)
fooTextCountCallsCount += 1
fooTextCountReceivedArguments = (text, count)
fooTextCountReceivedInvocations.append((text, count))
if fooTextCountClosure != nil {
return await fooTextCountClosure!(text, count)
} else {
return fooWithTextCountReturnValue
return fooTextCountReturnValue
}
}
}
Expand All @@ -178,22 +178,22 @@ final class UT_SpyFactory: XCTestCase {
result,
"""
class ServiceProtocolSpy: ServiceProtocol {
var fooWithAddedCallsCount = 0
var fooWithAddedCalled: Bool {
return fooWithAddedCallsCount > 0
var fooCallsCount = 0
var fooCalled: Bool {
return fooCallsCount > 0
}
var fooWithAddedReceivedAdded: ((text: String) -> Void)?
var fooWithAddedReceivedInvocations: [((text: String) -> Void)?] = []
var fooWithAddedReturnValue: (() -> Int)?
var fooWithAddedClosure: ((((text: String) -> Void)?) throws -> (() -> Int)?)?
var fooReceivedAdded: ((text: String) -> Void)?
var fooReceivedInvocations: [((text: String) -> Void)?] = []
var fooReturnValue: (() -> Int)?
var fooClosure: ((((text: String) -> Void)?) throws -> (() -> Int)?)?
func foo(_ added: ((text: String) -> Void)?) throws -> (() -> Int)? {
fooWithAddedCallsCount += 1
fooWithAddedReceivedAdded = (added)
fooWithAddedReceivedInvocations.append((added))
if fooWithAddedClosure != nil {
return try fooWithAddedClosure!(added)
fooCallsCount += 1
fooReceivedAdded = (added)
fooReceivedInvocations.append((added))
if fooClosure != nil {
return try fooClosure!(added)
} else {
return fooWithAddedReturnValue
return fooReturnValue
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions Tests/SpyableMacroTests/Factories/UT_VariablePrefixFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import XCTest
@testable import SpyableMacro
import SwiftSyntax

final class UT_VariablePrefixFactory: XCTestCase {
func testTextFunctionWithoutArguments() throws {
let declaration: DeclSyntax = "func foo() -> String"
let functionDeclaration = try XCTUnwrap(FunctionDeclSyntax(declaration))

let result = VariablePrefixFactory().text(for: functionDeclaration)

XCTAssertEqual(result, "foo")
}

func testTextFunctionWithSingleArgument() throws {
let declaration: DeclSyntax = "func foo(text: String) -> String"
let functionDeclaration = try XCTUnwrap(FunctionDeclSyntax(declaration))

let result = VariablePrefixFactory().text(for: functionDeclaration)

XCTAssertEqual(result, "fooText")
}

func testTextFunctionWithSingleArgumentTwoNames() throws {
let declaration: DeclSyntax = "func foo(generated text: String) -> String"
let functionDeclaration = try XCTUnwrap(FunctionDeclSyntax(declaration))

let result = VariablePrefixFactory().text(for: functionDeclaration)

XCTAssertEqual(result, "fooGenerated")
}

func testTextFunctionWithSingleArgumentOnlySecondName() throws {
let declaration: DeclSyntax = "func foo(_ text: String) -> String"
let functionDeclaration = try XCTUnwrap(FunctionDeclSyntax(declaration))

let result = VariablePrefixFactory().text(for: functionDeclaration)

XCTAssertEqual(result, "foo")
}

func testTextFunctionWithMultiArguments() throws {
let declaration: DeclSyntax = """
func foo(text1 text2: String, _ count2: Int, product1 product2: (name: String, price: Decimal)) -> String
"""
let functionDeclaration = try XCTUnwrap(FunctionDeclSyntax(declaration))

let result = VariablePrefixFactory().text(for: functionDeclaration)

XCTAssertEqual(result, "fooText1Product1")
}
}
Loading

0 comments on commit 9f4bd40

Please sign in to comment.