Skip to content

Commit

Permalink
Add Nested protocol support (#270)
Browse files Browse the repository at this point in the history
* Scan nested protocol and add test

* remove unused `data` property

* Avoid mock generation in generic context

* Render mocks in extension when the mock has namespaces

* A bit performance improve
  • Loading branch information
sidepelican authored Oct 30, 2024
1 parent 09a1ea2 commit 39e406f
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 48 deletions.
3 changes: 3 additions & 0 deletions Sources/MockoloFramework/Models/NominalModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class NominalModel: Model {
case `actor`
}

let namespaces: [String]
var name: String
var offset: Int64
var type: SwiftType
Expand All @@ -41,6 +42,7 @@ final class NominalModel: Model {
}

init(identifier: String,
namespaces: [String],
acl: String,
declTypeOfMockAnnotatedBaseType: DeclType,
declKind: NominalTypeDeclKind,
Expand All @@ -54,6 +56,7 @@ final class NominalModel: Model {
self.identifier = identifier
self.name = metadata?.nameOverride ?? (identifier + "Mock")
self.type = SwiftType(self.name)
self.namespaces = namespaces
self.declTypeOfMockAnnotatedBaseType = declTypeOfMockAnnotatedBaseType
self.declKind = declKind
self.inheritedTypes = inheritedTypes
Expand Down
9 changes: 3 additions & 6 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ struct ResolvedEntity {

func model() -> Model {
return NominalModel(identifier: key,
namespaces: entity.entityNode.namespaces,
acl: entity.entityNode.accessLevel,
declTypeOfMockAnnotatedBaseType: entity.entityNode.declType,
declKind: inheritsActorProtocol ? .actor : .class,
Expand All @@ -84,14 +85,15 @@ struct ResolvedEntityContainer {
}

protocol EntityNode {
var namespaces: [String] { get }
var nameText: String { get }
var accessLevel: String { get }
var attributesDescription: String { get }
var declType: DeclType { get }
var inheritedTypes: [String] { get }
var offset: Int64 { get }
var hasBlankInit: Bool { get }
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, isProcessed: Bool) -> EntityNodeSubContainer
}

final class EntityNodeSubContainer {
Expand Down Expand Up @@ -139,7 +141,6 @@ public typealias ImportMap = [String: [String: [String]]]
/// Metadata for a type being mocked
public final class Entity {
var filepath: String = ""
var data: Data? = nil
let entityNode: EntityNode
let isProcessed: Bool
let metadata: AnnotationMetadata?
Expand All @@ -150,7 +151,6 @@ public final class Entity {

static func node(with entityNode: EntityNode,
filepath: String,
data: Data? = nil,
isPrivate: Bool,
isFinal: Bool,
metadata: AnnotationMetadata?,
Expand All @@ -160,7 +160,6 @@ public final class Entity {

let node = Entity(entityNode: entityNode,
filepath: filepath,
data: data,
metadata: metadata,
isProcessed: processed)

Expand All @@ -169,12 +168,10 @@ public final class Entity {

init(entityNode: EntityNode,
filepath: String = "",
data: Data? = nil,
metadata: AnnotationMetadata?,
isProcessed: Bool) {
self.entityNode = entityNode
self.filepath = filepath
self.data = data
self.metadata = metadata
self.isProcessed = isProcessed
}
Expand Down
75 changes: 55 additions & 20 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ extension IfConfigDeclSyntax {
}

extension ProtocolDeclSyntax: EntityNode {
var namespaces: [String] {
return findNamespaces(parent: parent)
}

var nameText: String {
return name.text
}
Expand Down Expand Up @@ -295,12 +299,15 @@ extension ProtocolDeclSyntax: EntityNode {
return false
}

func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer {
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, isProcessed: Bool) -> EntityNodeSubContainer {
return self.memberBlock.members.memberData(with: accessLevel, declType: declType, metadata: metadata, processed: isProcessed)
}
}

extension ClassDeclSyntax: EntityNode {
var namespaces: [String] {
return findNamespaces(parent: parent)
}

var nameText: String {
return name.text
Expand Down Expand Up @@ -346,11 +353,34 @@ extension ClassDeclSyntax: EntityNode {
return leadingTrivia.annotationMetadata(with: annotation)
}

func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer {
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, isProcessed: Bool) -> EntityNodeSubContainer {
return self.memberBlock.members.memberData(with: accessLevel, declType: declType, metadata: nil, processed: isProcessed)
}
}

fileprivate func findNamespaces(parent: Syntax?) -> [String] {
guard let parent else {
return []
}
return sequence(first: parent, next: \.parent)
.compactMap { element in
if let decl = element.as(StructDeclSyntax.self) {
return decl.name.trimmedDescription
} else if let decl = element.as(EnumDeclSyntax.self) {
return decl.name.trimmedDescription
} else if let decl = element.as(ClassDeclSyntax.self) {
return decl.name.trimmedDescription
} else if let decl = element.as(ActorDeclSyntax.self) {
return decl.name.trimmedDescription
} else if let decl = element.as(ExtensionDeclSyntax.self) {
return decl.extendedType.trimmedDescription
} else {
return nil
}
}
.reversed()
}

extension VariableDeclSyntax {
func models(with acl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> [Model] {
// Detect whether it's static
Expand Down Expand Up @@ -618,19 +648,23 @@ final class EntityVisitor: SyntaxVisitor {
super.init(viewMode: .sourceAccurate)
}

override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { visitImpl(node) }

private func visitImpl(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
let metadata = node.annotationMetadata(with: annotation)
if let ent = Entity.node(with: node, filepath: path, isPrivate: node.isPrivate, isFinal: false, metadata: metadata, processed: false) {
entities.append(ent)
}
return .skipChildren
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { visitImpl(node) }
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
return node.genericParameterClause != nil ? .skipChildren : .visitChildren
}

private func visitImpl(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
return node.genericParameterClause != nil ? .skipChildren : .visitChildren
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
if node.nameText.hasSuffix("Mock") {
// this mock class node must be public else wouldn't have compiled before
if let ent = Entity.node(with: node, filepath: path, isPrivate: node.isPrivate, isFinal: false, metadata: nil, processed: true) {
Expand All @@ -644,25 +678,22 @@ final class EntityVisitor: SyntaxVisitor {
}
}
}
return .skipChildren
return node.genericParameterClause != nil ? .skipChildren : .visitChildren
}

override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { visitImpl(node) }
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
return node.genericParameterClause != nil ? .skipChildren : .visitChildren
}

private func visitImpl(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
if let ret = node.path.firstToken(viewMode: .sourceAccurate)?.text {
let desc = node.importKeyword.text + " " + ret
if imports[""] == nil {
imports[""] = []
}
imports[""]?.append(desc)
imports["", default: []].append(desc)
}
return .visitChildren
return .skipChildren
}

override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { visitImpl(node) }

private func visitImpl(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
for cl in node.clauses {
let macroName: String
if let conditionDescription = cl.condition?.trimmedDescription {
Expand Down Expand Up @@ -697,11 +728,15 @@ final class EntityVisitor: SyntaxVisitor {
return .skipChildren
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
return .skipChildren
}

override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
return .skipChildren
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
return .skipChildren
}
}
Expand Down
12 changes: 10 additions & 2 deletions Sources/MockoloFramework/Templates/NominalTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,16 @@ extension NominalModel {
\(body)
}
"""

return template

if namespaces.isEmpty {
return template
} else {
return """
extension \(namespaces.joined(separator: ".")) {
\(template.addingIndent(1))
}
"""
}
}

private func extraInitsIfNeeded(
Expand Down
4 changes: 1 addition & 3 deletions Sources/MockoloFramework/Templates/VariableTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ extension VariableModel {
returnType: type,
encloser: ""
).render(with: name, encloser: "") ?? "")
.split(separator: "\n")
.map { "\(1.tab)\($0)" }
.joined(separator: "\n")
.addingIndent(1)

return """
Expand Down
10 changes: 2 additions & 8 deletions Sources/MockoloFramework/Utils/InheritanceResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,12 @@ func lookupEntities(key: String,

// Look up the mock entities of a protocol specified by the name.
if let current = protocolMap[key] {
let sub = current.entityNode.subContainer(metadata: current.metadata, declType: declType, path: current.filepath, data: current.data, isProcessed: current.isProcessed)
let sub = current.entityNode.subContainer(metadata: current.metadata, declType: declType, path: current.filepath, isProcessed: current.isProcessed)
models.append(contentsOf: sub.members)
if !current.isProcessed {
attributes.append(contentsOf: sub.attributes)
}
inheritedTypes.formUnion(current.entityNode.inheritedTypes)
if let data = current.data {
pathToContents.append((current.filepath, data, current.entityNode.offset))
}
paths.append(current.filepath)


Expand All @@ -72,14 +69,11 @@ func lookupEntities(key: String,
}
} else if let parentMock = inheritanceMap["\(key)Mock"], declType == .protocolType {
// If the parent protocol is not in the protocol map, look it up in the input parent mocks map.
let sub = parentMock.entityNode.subContainer(metadata: parentMock.metadata, declType: declType, path: parentMock.filepath, data: parentMock.data, isProcessed: parentMock.isProcessed)
let sub = parentMock.entityNode.subContainer(metadata: parentMock.metadata, declType: declType, path: parentMock.filepath, isProcessed: parentMock.isProcessed)
processedModels.append(contentsOf: sub.members)
if !parentMock.isProcessed {
attributes.append(contentsOf: sub.attributes)
}
if let data = parentMock.data {
pathToContents.append((parentMock.filepath, data, parentMock.entityNode.offset))
}
paths.append(parentMock.filepath)
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/MockoloFramework/Utils/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ extension String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
}

func addingIndent(_ tabs: Int) -> String {
self.split(separator: "\n")
.map { line in
if line.isEmpty {
return ""
}
return "\(tabs.tab)\(line)"
}
.joined(separator: "\n")
}
}

let separatorsForDisplay = CharacterSet(charactersIn: "<>[] :,()_-.&@#!{}@+\"\'")
Expand Down
9 changes: 0 additions & 9 deletions Tests/TestModuleNames/ModuleNameTests.swift

This file was deleted.

Loading

0 comments on commit 39e406f

Please sign in to comment.