Skip to content

Commit

Permalink
Introduce FileVisitor
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Dec 2, 2023
1 parent 93e1cc9 commit 57df143
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Sources/SafeDICore/Models/Instantiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

struct Instantiable: Codable {
struct Instantiable: Codable, Hashable {

// MARK: Initialization

Expand Down
123 changes: 123 additions & 0 deletions Sources/SafeDICore/Visitors/FileVisitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftSyntax

// @DAN TODO: write tests! cover nested @Instantiable types too
/// A visitor that can read entire files. A single `FileVisitor` can be used to walk every file in a module.
final class FileVisitor: SyntaxVisitor {

// MARK: Initialization

init() {
super.init(viewMode: .sourceAccurate)
}

// MARK: SyntaxVisitor

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

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

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

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

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

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
visitDecl(node)
}

override func visitPost(_ node: ClassDeclSyntax) {
visitPostDecl(node)
}

override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
visitDecl(node)
}

override func visitPost(_ node: ActorDeclSyntax) {
visitPostDecl(node)
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
visitDecl(node)
}

override func visitPost(_ node: StructDeclSyntax) {
visitPostDecl(node)
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
declSyntaxParentCount += 1
return .visitChildren // Make sure there aren't `@Instantiable`s declared within an enum.
}

override func visitPost(_ node: EnumDeclSyntax) {
declSyntaxParentCount -= 1
}

// MARK: Internal

var instantiables = [Instantiable]()
var disallowedInstantiableDecoratedTypeDescriptions = [TypeDescription]()

// MARK: Private

private var declSyntaxParentCount = 0

private func visitDecl(_ node: some ConcreteDeclSyntaxProtocol) -> SyntaxVisitorContinueKind {
// TODO: Allow Instantiable to be nested types. Accomplishing this task will require understanding when other nested types are being referenced.
guard declSyntaxParentCount == 0 else {
let instantiableVisitor = InstantiableVisitor()
instantiableVisitor.walk(node)
if let instantiableType = instantiableVisitor.instantiableType {
disallowedInstantiableDecoratedTypeDescriptions.append(instantiableType)
}
return .visitChildren
}

let instantiableVisitor = InstantiableVisitor()
instantiableVisitor.walk(node)
if let instantiable = instantiableVisitor.instantiable {
instantiables.append(instantiable)
}

declSyntaxParentCount += 1

// Find nested Instantiable types.
return .visitChildren
}

private func visitPostDecl(_ node: some ConcreteDeclSyntaxProtocol) {
declSyntaxParentCount -= 1
}
}
183 changes: 183 additions & 0 deletions Tests/SafeDICoreTests/FileVisitorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftSyntax
import SwiftParser
import XCTest

@testable import SafeDICore

final class FileVisitorTests: XCTestCase {

func test_walk_findsTopLevelInstantiable() {
let fileVisitor = FileVisitor()
fileVisitor.walk(Parser.parse(source: """
import UIKit
@Instantiable
public final class LoggedInViewController: UIViewController {
init(user: User, networkService: NetworkService) {
self.user = user
self.networkService = networkService
}
@Forwarded
private let user: User
@Inherited
let networkService: NetworkService
}
"""))
XCTAssertEqual(
fileVisitor.instantiables,
[
Instantiable(
instantiableType: .simple(name: "LoggedInViewController"),
additionalInstantiableTypes: nil,
dependencies: [
Dependency(
property: Property(
label: "user",
typeDescription: .simple(name: "User")
),
source: .forwarded
),
Dependency(
property: Property(
label: "networkService",
typeDescription: .simple(name: "NetworkService")
),
source: .inherited
)
])
]
)
XCTAssertEqual(
fileVisitor.disallowedInstantiableDecoratedTypeDescriptions,
[]
)
}

func test_walk_findsMultipleTopLevelInstantiables() {
let fileVisitor = FileVisitor()
fileVisitor.walk(Parser.parse(source: """
@Instantiable
public final class LoggedInViewController: UIViewController {
init(user: User, networkService: NetworkService) {
self.user = user
self.networkService = networkService
}
@Forwarded
private let user: User
@Inherited
let networkService: NetworkService
}
@Instantiable
struct SomeOtherInstantiable {}
"""))
XCTAssertEqual(
fileVisitor.instantiables,
[
Instantiable(
instantiableType: .simple(name: "LoggedInViewController"),
additionalInstantiableTypes: nil,
dependencies: [
Dependency(
property: Property(
label: "user",
typeDescription: .simple(name: "User")
),
source: .forwarded
),
Dependency(
property: Property(
label: "networkService",
typeDescription: .simple(name: "NetworkService")
),
source: .inherited
)
]),
Instantiable(
instantiableType: .simple(name: "SomeOtherInstantiable"),
additionalInstantiableTypes: nil,
dependencies: []
)
]
)
XCTAssertEqual(
fileVisitor.disallowedInstantiableDecoratedTypeDescriptions,
[]
)
}

func test_walk_errorsOnNestedInstantiable() {
let fileVisitor = FileVisitor()
fileVisitor.walk(Parser.parse(source: """
@Instantiable(fulfillingAdditionalTypes: [SomeProtocol.self])
public struct OuterLevel: SomeProtocol {
@Instantiable
public struct InnerLevel {}
}
"""))
XCTAssertEqual(
fileVisitor.instantiables,
[
Instantiable(
instantiableType: .simple(name: "OuterLevel"),
additionalInstantiableTypes: [
.simple(name: "SomeProtocol")
],
dependencies: []
)
]
)
XCTAssertEqual(
fileVisitor.disallowedInstantiableDecoratedTypeDescriptions,
[
.simple(name: "InnerLevel")
]
)
}

func test_walk_errorsOnInstantiableNestedWithinEnum() {
let fileVisitor = FileVisitor()
fileVisitor.walk(Parser.parse(source: """
public enum OuterLevel {
@Instantiable
public struct InnerLevel {}
}
"""))
XCTAssertEqual(
fileVisitor.instantiables,
[]
)
XCTAssertEqual(
fileVisitor.disallowedInstantiableDecoratedTypeDescriptions,
[
.simple(name: "InnerLevel")
]
)
}
}
2 changes: 1 addition & 1 deletion Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ final class InstantiableMacroTests: XCTestCase {
assertMacroExpansion(
"""
@Instantiable(fulfillingAdditionalTypes: [ExampleService.self])
public struct DefaultExampleService: ExampleService {
public actor DefaultExampleService: ExampleService {
init(
invariantA: InvariantA,
invariantB: InvariantB,
Expand Down

0 comments on commit 57df143

Please sign in to comment.