diff --git a/Sources/Rubicon/Domain/EnumDeclaration.swift b/Sources/Rubicon/Domain/EnumDeclaration.swift new file mode 100644 index 0000000..8d14d95 --- /dev/null +++ b/Sources/Rubicon/Domain/EnumDeclaration.swift @@ -0,0 +1,6 @@ +public struct EnumDeclaration: Equatable { + public let name: String + public let cases: [String] + public let notes: [String] + public let accessLevel: AccessLevel +} diff --git a/Sources/Rubicon/Generator/EnumStubGenerator.swift b/Sources/Rubicon/Generator/EnumStubGenerator.swift new file mode 100644 index 0000000..c410082 --- /dev/null +++ b/Sources/Rubicon/Generator/EnumStubGenerator.swift @@ -0,0 +1,66 @@ +public protocol EnumStubGenerator { + func generate(from enumType: EnumDeclaration, functionName: String) -> String +} + +final class EnumStubGeneratorImpl: EnumStubGenerator { + private let extensionGenerator: ExtensionGenerator + private let functionGenerator: FunctionGenerator + private let indentationGenerator: IndentationGenerator + private let defaultValueGenerator: DefaultValueGenerator + + init( + extensionGenerator: ExtensionGenerator, + functionGenerator: FunctionGenerator, + indentationGenerator: IndentationGenerator, + defaultValueGenerator: DefaultValueGenerator + ) { + self.extensionGenerator = extensionGenerator + self.functionGenerator = functionGenerator + self.indentationGenerator = indentationGenerator + self.defaultValueGenerator = defaultValueGenerator + } + + func generate(from enumType: EnumDeclaration, functionName: String) -> String { + let content = generateBody(from: enumType, functionName: functionName) + return extensionGenerator.make( + name: enumType.name, + content: content + ).joined(separator: "\n") + "\n" + } + + private func generateBody(from enumType: EnumDeclaration, functionName: String) -> [String] { + let content = makeContent(from: enumType) + let functionDeclaration = FunctionDeclaration( + name: functionName, + arguments: [], + isThrowing: false, + isAsync: false, + isStatic: true, + returnType: TypeDeclaration(name: enumType.name, prefix: [], composedType: .plain) + ) + return functionGenerator.makeCode( + from: functionDeclaration, + content: content, + isEachArgumentOnNewLineEnabled: true + ) + } + + private func makeContent(from enumType: EnumDeclaration) -> [String] { + let firstCase = enumType.cases.first.map { "." + $0 } ?? "" + return [ + "return \(firstCase)" + ] + } + + private func makeAssigment(of variable: VarDeclaration, isLast: Bool) -> String { + return "\(variable.identifier): \(variable.identifier)\(isLast ? "" : ",")" + } + + private func makeArgument(from varDeclaration: VarDeclaration) -> ArgumentDeclaration { + ArgumentDeclaration( + name: varDeclaration.identifier, + type: varDeclaration.type, + defaultValue: defaultValueGenerator.makeDefaultValue(for: varDeclaration) + ) + } +} diff --git a/Sources/Rubicon/Integration/Rubicon.swift b/Sources/Rubicon/Integration/Rubicon.swift index 1ea97ef..7c9473b 100644 --- a/Sources/Rubicon/Integration/Rubicon.swift +++ b/Sources/Rubicon/Integration/Rubicon.swift @@ -279,4 +279,32 @@ public final class Rubicon { ) ) } + + public func makeEnumParser() -> EnumParser { + return EnumParserImpl() + } + + public func makeEnumGenerator(for configuration: StructStubConfiguration) -> EnumStubGenerator { + let dependencies = makeDependencies( + for: configuration.accessLevel, + indentStep: configuration.indentStep + ) + return EnumStubGeneratorImpl( + extensionGenerator: ExtensionGeneratorImpl( + accessLevelGenerator: dependencies.accessLevelGenerator, + indentationGenerator: dependencies.indentationGenerator + ), + functionGenerator: FunctionGeneratorImpl( + accessLevelGenerator: dependencies.accessLevelGenerator, + typeGenerator: dependencies.typeGenerator, + argumentGenerator: dependencies.argumentGenerator, + indentationGenerator: dependencies.indentationGenerator + ), + indentationGenerator: dependencies.indentationGenerator, + defaultValueGenerator: DefaultValueGeneratorImpl( + unknownDefaultType: configuration.defaultValue, + customDefaultTypes: configuration.customDefaultValues + ) + ) + } } diff --git a/Sources/Rubicon/Syntactic analysis/EnumParser.swift b/Sources/Rubicon/Syntactic analysis/EnumParser.swift new file mode 100644 index 0000000..15fe619 --- /dev/null +++ b/Sources/Rubicon/Syntactic analysis/EnumParser.swift @@ -0,0 +1,115 @@ +import SwiftParser +import SwiftSyntax + +public protocol EnumParser { + func parse(text: String) throws -> [EnumDeclaration] +} + +final class EnumParserImpl: EnumParser { + func parse(text: String) throws -> [EnumDeclaration] { + let text = SwiftParser.Parser.parse(source: text) + let visitor = EnumVisitor( + nestedInItemsNames: [] + ) + return visitor.execute(node: text) + } +} + +private class EnumVisitor: SyntaxVisitor { + private var result = [EnumDeclaration]() + private var nestedInItemsNames: [String] + + public init( + nestedInItemsNames: [String] + ) { + self.nestedInItemsNames = nestedInItemsNames + super.init(viewMode: .sourceAccurate) + } + + func execute(node: some SyntaxProtocol) -> [EnumDeclaration] { + walk(node) + return result + } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + + let name = node.name.text + + result.append( + EnumDeclaration( + name: (nestedInItemsNames + [name]).joined(separator: "."), + cases: parseCases(node: node.memberBlock), + notes: node.leadingTrivia.pieces.compactMap(makeLineComment), + accessLevel: parseAccessLevel(from: node) + ) + ) + + let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name]) + result += subVisitor.execute(node: node.memberBlock.members) + + return .skipChildren + } + + private func parseCases(node: MemberBlockSyntax) -> [String] { + let casesParser = CasesVisitor(viewMode: .sourceAccurate) + return casesParser.execute(node: node.members) + } + + private func parseAccessLevel(from node: EnumDeclSyntax) -> AccessLevel { + let modifiers = node.modifiers.map { $0.name.tokenKind } + + if modifiers.contains(.keyword(.public)) { + return .public + } else if modifiers.contains(.keyword(.private)) { + return .private + } else { + return .internal + } + } + + private func makeLineComment(from triviaPiece: TriviaPiece) -> String? { + switch triviaPiece { + case .lineComment(let text): + text + default: + nil + } + } + + private func parseParent(from inheridedTypeSyntax: InheritedTypeSyntax) -> String? { + guard let type = inheridedTypeSyntax.type.as(IdentifierTypeSyntax.self) else { + return nil + } + + return type.name.text + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + let name = node.name.text + let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name]) + result += subVisitor.execute(node: node.memberBlock.members) + return .skipChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + let name = node.name.text + let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name]) + result += subVisitor.execute(node: node.memberBlock.members) + return .skipChildren + } +} + +private class CasesVisitor: SyntaxVisitor { + private var result = [String]() + + func execute(node: some SyntaxProtocol) -> [String] { + walk(node) + return result + } + + override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { + let declaration = node.elements.map(\.name.text) + result += declaration + return .visitChildren + } +} diff --git a/Tests/RubiconTests/Generator/EnumStubGeneratorTests.swift b/Tests/RubiconTests/Generator/EnumStubGeneratorTests.swift new file mode 100644 index 0000000..9dde2c2 --- /dev/null +++ b/Tests/RubiconTests/Generator/EnumStubGeneratorTests.swift @@ -0,0 +1,69 @@ +@testable import Rubicon +import XCTest + +final class EnumStubGeneratorTests: XCTestCase { + private var extensionGeneratorSpy: ExtensionGeneratorSpy! + private var functionGeneratorSpy: FunctionGeneratorSpy! + private var indentationGeneratorStub: IndentationGeneratorStub! + private var defaultValueGeneratorSpy: DefaultValueGeneratorSpy! + private var sut: EnumStubGeneratorImpl! + + override func setUp() { + super.setUp() + extensionGeneratorSpy = ExtensionGeneratorSpy(makeReturn: ["extension"]) + functionGeneratorSpy = FunctionGeneratorSpy(makeCodeReturn: ["function"]) + indentationGeneratorStub = IndentationGeneratorStub() + defaultValueGeneratorSpy = DefaultValueGeneratorSpy(makeDefaultValueReturn: "default") + sut = EnumStubGeneratorImpl( + extensionGenerator: extensionGeneratorSpy, + functionGenerator: functionGeneratorSpy, + indentationGenerator: IndentationGeneratorStub(), + defaultValueGenerator: defaultValueGeneratorSpy + ) + } + + func test_givenEmptyEnum_whenMakeCode_thenReturnCode() { + let declaration = EnumDeclaration.makeStub(cases: []) + + let code = sut.generate(from: declaration, functionName: "functionName") + + XCTAssertEqual(code, "extension\n") + XCTAssertEqual(extensionGeneratorSpy.make.count, 1) + XCTAssertEqual(extensionGeneratorSpy.make.first?.content, ["function"]) + XCTAssertEqual(functionGeneratorSpy.makeCode.count, 1) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.name, "functionName") + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isThrowing, false) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isAsync, false) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isStatic, true) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.arguments.count, 0) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.returnType, .makeStub(name: "EnumName")) + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.content, ["return "]) + } + + func test_givenVariableEnum_whenMakeCode_thenReturnCode() { + let declaration = EnumDeclaration.makeStub(cases: [ + "a", + "b" + ]) + + _ = sut.generate(from: declaration, functionName: "functionName") + + XCTAssertEqual(functionGeneratorSpy.makeCode.first?.content, ["return .a"]) + } +} + +extension EnumDeclaration { + static func makeStub( + cases: [String] = [] + ) -> EnumDeclaration { + return EnumDeclaration( + name: "EnumName", + cases: cases, + notes: [ + "note1", + "note2" + ], + accessLevel: .internal + ) + } +} diff --git a/Tests/RubiconTests/Syntactic analysis/EnumParserTests.swift b/Tests/RubiconTests/Syntactic analysis/EnumParserTests.swift new file mode 100644 index 0000000..84b3e0d --- /dev/null +++ b/Tests/RubiconTests/Syntactic analysis/EnumParserTests.swift @@ -0,0 +1,160 @@ +@testable import Rubicon +import SwiftParser +import SwiftSyntax +import XCTest + +final class EnumParserTests: XCTestCase { + private var sut: EnumParserImpl! + + override func setUp() { + super.setUp() + sut = EnumParserImpl() + } + + func test_givenNoEnum_whenParse_thenReturnEmptyResult() throws { + let text = """ + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 0) + } + + func test_givenEmptyEnum_whenParse_thenReturnEnum() throws { + let text = """ + enum A { + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 1) + XCTAssertEqual(enums.first?.name, "A") + XCTAssertEqual(enums.first?.cases.count, 0) + XCTAssertEqual(enums.first?.notes, []) + XCTAssertEqual(enums.first?.accessLevel, .internal) + } + + func test_givenEnumWithVariables_whenParse_thenReturnEnum() throws { + let text = """ + enum A { + case a + case b + case c,d + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 1) + XCTAssertEqual(enums.first?.name, "A") + XCTAssertEqual(enums.first?.cases.count, 4) + XCTAssertEqual(enums.first?.cases.first, "a") + } + + func test_givenEnumNestedInClass_whenParse_thenReturnEnum() throws { + let text = """ + class B { + enum A { + } + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 1) + XCTAssertEqual(enums.first?.name, "B.A") + XCTAssertEqual(enums.first?.cases.count, 0) + } + + func test_givenEnumNestedInStruct_whenParse_thenReturnEnum() throws { + let text = """ + struct B { + enum A { + } + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 1) + XCTAssertEqual(enums.first?.name, "B.A") + XCTAssertEqual(enums.first?.cases.count, 0) + } + + func test_givenEnumNestedInEnum_whenParse_thenReturnEnum() throws { + let text = """ + enum B { + enum A { + } + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 2) + XCTAssertEqual(enums.first?.name, "B") + XCTAssertEqual(enums.first?.cases.count, 0) + XCTAssertEqual(enums.last?.name, "B.A") + XCTAssertEqual(enums.last?.cases.count, 0) + } + + func test_givenEnumNestedIntoMultipleItems_whenParse_thenReturnEnum() throws { + let text = """ + class B { + struct C { + enum D { + enum A { + } + } + } + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 2) + XCTAssertEqual(enums.first?.name, "B.C.D") + XCTAssertEqual(enums.first?.cases.count, 0) + XCTAssertEqual(enums.last?.name, "B.C.D.A") + } + + func test_givenEmptyWithNote_whenParse_thenReturnEnum() throws { + let text = """ + // NOTE + enum A { + } + """ + + let enums = try sut.parse(text: text) + + XCTAssertEqual(enums.count, 1) + XCTAssertEqual(enums.first?.name, "A") + XCTAssertEqual(enums.first?.cases.count, 0) + XCTAssertEqual(enums.first?.notes, ["// NOTE"]) + } + + func test_givenPublicEnum_whenParse_thenReturnEnum() throws { + let text = """ + public enum A { + } + """ + + let enums = try sut.parse(text: text) + + + XCTAssertEqual(enums.first?.accessLevel, .public) + } + + func test_givenPrivateEnum_whenParse_thenReturnEnum() throws { + let text = """ + private enum A { + } + """ + + let enums = try sut.parse(text: text) + + + XCTAssertEqual(enums.first?.accessLevel, .private) + } +}