Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/SafeDICore/Visitors/FileVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,16 @@ public final class FileVisitor: SyntaxVisitor {
parentType = nil
}

public override func visit(_: UnexpectedNodesSyntax) -> SyntaxVisitorContinueKind {
encounteredUnexpectedNodesSyntax = true
return .skipChildren
}

// MARK: Public

public private(set) var imports = [ImportStatement]()
public private(set) var instantiables = [Instantiable]()
public private(set) var encounteredUnexpectedNodesSyntax = false

// MARK: Private

Expand Down
65 changes: 43 additions & 22 deletions Sources/SafeDITool/SafeDITool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,28 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
: nil

if let moduleInfoOutput {
try JSONEncoder().encode(ModuleInfo(
imports: module.imports,
instantiables: module.instantiables
)).write(toPath: moduleInfoOutput)
try JSONEncoder().encode(module).write(toPath: moduleInfoOutput)
}

if let dependencyTreeOutput,
let generatedCode = try await generatedCode,
// Only update the file if the file has changed.
await existingGeneratedCode != generatedCode
{
try generatedCode.write(toPath: dependencyTreeOutput)
if let dependencyTreeOutput {
let filesWithUnexpectedNodes = dependentModuleInfo.compactMap(\.filesWithUnexpectedNodes).flatMap(\.self) + (module.filesWithUnexpectedNodes ?? [])
if !filesWithUnexpectedNodes.isEmpty {
try """
// This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
// Any modifications made to this file will be overwritten on subsequent builds.
// Please refrain from editing this file directly.

#error(\"""
Compiler errors prevented the generation of the dependency tree. Files with errors:
\(filesWithUnexpectedNodes.joined(separator: "\n\t"))
\""")
""".write(toPath: dependencyTreeOutput)
} else if let generatedCode = try await generatedCode,
// Only update the file if the file has changed.
await existingGeneratedCode != generatedCode
{
try generatedCode.write(toPath: dependencyTreeOutput)
}
}

if let dotFileOutput {
Expand All @@ -154,6 +164,7 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
struct ModuleInfo: Codable, Sendable {
let imports: [ImportStatement]
let instantiables: [Instantiable]
let filesWithUnexpectedNodes: [String]?
}

@TaskLocal static var fileFinder: FileFinder = FileManager.default
Expand Down Expand Up @@ -213,34 +224,49 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
}
}

private func parsedModule() async throws -> ParsedModule {
private func parsedModule() async throws -> ModuleInfo {
try await withThrowingTaskGroup(
of: (imports: [ImportStatement], instantiables: [Instantiable])?.self,
returning: ParsedModule.self
of: (
imports: [ImportStatement],
instantiables: [Instantiable],
encounteredUnexpectedNodeInFile: String?
)?.self,
returning: ModuleInfo.self
) { taskGroup in
var imports = [ImportStatement]()
var instantiables = [Instantiable]()
var filesWithUnexpectedNodes = [String]()
for filePath in try await findSwiftFiles() where !filePath.isEmpty {
taskGroup.addTask {
let content = try String(contentsOfFile: filePath, encoding: .utf8)
guard content.contains("@\(InstantiableVisitor.macroName)") else { return nil }
let fileVisitor = FileVisitor()
fileVisitor.walk(Parser.parse(source: content))
guard !fileVisitor.instantiables.isEmpty else { return nil }
return (imports: fileVisitor.imports, instantiables: fileVisitor.instantiables)
guard !fileVisitor.instantiables.isEmpty
|| fileVisitor.encounteredUnexpectedNodesSyntax
else { return nil }
return (
imports: fileVisitor.imports,
instantiables: fileVisitor.instantiables,
encounteredUnexpectedNodeInFile: fileVisitor.encounteredUnexpectedNodesSyntax ? filePath : nil
)
}
}

for try await fileInfo in taskGroup {
if let fileInfo {
imports.append(contentsOf: fileInfo.imports)
instantiables.append(contentsOf: fileInfo.instantiables)
if let filePath = fileInfo.encounteredUnexpectedNodeInFile {
filesWithUnexpectedNodes.append(filePath)
}
}
}

return ParsedModule(
return ModuleInfo(
imports: imports,
instantiables: instantiables
instantiables: instantiables,
filesWithUnexpectedNodes: filesWithUnexpectedNodes.isEmpty ? nil : filesWithUnexpectedNodes
)
}
}
Expand Down Expand Up @@ -320,11 +346,6 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
return typeDescriptionToFulfillingInstantiableMap
}

private struct ParsedModule {
let imports: [ImportStatement]
let instantiables: [Instantiable]
}

private enum CollectInstantiablesError: Error, CustomStringConvertible {
case foundDuplicateInstantiable(String)

Expand Down
47 changes: 47 additions & 0 deletions Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5878,6 +5878,53 @@ struct SafeDIToolCodeGenerationTests: ~Copyable {
)
}

@Test
mutating func run_doesNotWriteConvenienceExtensionOnRootOfTree_whenUnexpectedSwiftNodesAreEncountered() async throws {
let output = try await executeSafeDIToolTest(
swiftFileContent: [
"""
@Instantiable
public final class A: Instantiable {
public init() {}

private var foo: Foo {
.init(
a: {},
b: {} // oh no! Missing trailing comma!
c: {},
d: { [weak self] in
guard let self else { return }
doSomething()
},
)
}

func doSomething() {}
}

@Instantiable
public final class B: Instantiable {
public init() {}
}
""",
],
buildDependencyTreeOutput: true,
filesToDelete: &filesToDelete
)

#expect(try #require(output.dependencyTree) == """
// This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
// Any modifications made to this file will be overwritten on subsequent builds.
// Please refrain from editing this file directly.

#error(\"""
Compiler errors prevented the generation of the dependency tree. Files with errors:
\(try #require(output.moduleInfo.filesWithUnexpectedNodes).joined(separator: "\n\t"))
\""")
"""
)
}

// MARK: Private

private var filesToDelete = [URL]()
Expand Down