Skip to content

Commit d8b38dc

Browse files
alexey1312google-labs-jules[bot]claude
authored
feat: Update swift-toml to 2.0.0 with improved error reporting (#55)
* Perf: Optimize directory scanning for coverage and test binaries - Replaced `for` loop with `while` loop using `enumerator.nextObject()` in `findXCResultBundles` and `findTestBinary`. - Added `enumerator.skipDescendants()` to prevent unnecessary recursion into `.xcresult` and `.xctest` bundles. - This reduces file system I/O by avoiding traversal of thousands of files inside these bundles. * chore: Update swift-toml to 2.0.0, remove C++ interop - Update swift-toml dependency from 1.0.0 to 2.0.0 - Remove .interoperabilityMode(.Cxx) from Package.swift - Remove cxxLanguageStandard: .cxx17 from Package.swift - Remove #if canImport(TOML) conditionals from ConfigLoader.swift - Remove tomlNotAvailable error case (no longer needed) - Update CLAUDE.md documentation to remove C++ references https://claude.ai/code/session_01WZkuS9vpvB2rcTZsstvubN * fix: Handle TOMLDecodingError for precise syntax error location swift-toml 2.0 throws TOMLDecodingError.invalidSyntax with line/column info for TOML parsing errors. Previously this was caught by the generic catch block and line/column info was lost. Now ConfigLoader properly extracts line and column from TOMLDecodingError, providing users with precise error locations in their config files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: update | Package.resolved --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent da4bd90 commit d8b38dc

File tree

5 files changed

+99
-123
lines changed

5 files changed

+99
-123
lines changed

CLAUDE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ The codebase follows a modular architecture:
256256
- `Configuration` struct (Codable) for TOML config file parsing
257257
- `ConfigLoader` handles file discovery and parsing with user-friendly errors
258258
- `ConfigMerger` merges config file values with CLI arguments (CLI takes precedence)
259-
- Uses swift-toml library for TOML parsing with C++ interoperability
259+
- Uses swift-toml library for TOML parsing
260260

261261
### Data Flow
262262
1. Stdin input → `readStandardInput()`
@@ -469,7 +469,6 @@ swift test --filter OutputParserTests.testParseError
469469
- **Swift ArgumentParser**: CLI argument handling (Package.swift dependency)
470470
- **ToonFormat** (toon-swift): Token-Oriented Object Notation encoder for efficient LLM output (Package.swift dependency)
471471
- **swift-toml**: TOML configuration file parsing with Codable support (Package.swift dependency)
472-
- Requires C++ interoperability mode (`swiftSettings: [.interoperabilityMode(.Cxx)]`)
473472
- **Foundation**: Core Swift framework for regex, JSON encoding, string processing
474473
- **XCTest**: Testing framework (test target only)
475474

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ let package = Package(
1414
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
1515
.package(url: "https://github.com/toon-format/toon-swift.git", from: "0.3.0"),
1616
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.5"),
17-
.package(url: "https://github.com/mattt/swift-toml", from: "1.0.0"),
17+
.package(url: "https://github.com/mattt/swift-toml", from: "2.0.0"),
1818
],
1919
targets: [
2020
.executableTarget(
@@ -23,9 +23,6 @@ let package = Package(
2323
.product(name: "ArgumentParser", package: "swift-argument-parser"),
2424
.product(name: "ToonFormat", package: "toon-swift"),
2525
.product(name: "TOML", package: "swift-toml"),
26-
],
27-
swiftSettings: [
28-
.interoperabilityMode(.Cxx)
2926
]
3027
),
3128
.testTarget(
@@ -36,11 +33,7 @@ let package = Package(
3633
.copy("Fixtures/build.txt"),
3734
.copy("Fixtures/swift-testing-output.txt"),
3835
.copy("Fixtures/linker-error-output.txt"),
39-
],
40-
swiftSettings: [
41-
.interoperabilityMode(.Cxx)
4236
]
4337
),
44-
],
45-
cxxLanguageStandard: .cxx17
38+
]
4639
)

Sources/ConfigLoader.swift

Lines changed: 89 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import Foundation
2-
3-
#if canImport(TOML)
4-
import TOML
5-
#endif
2+
import TOML
63

74
// MARK: - Config Loader Errors
85

@@ -13,8 +10,6 @@ enum ConfigError: Error, CustomStringConvertible {
1310
/// Reserved for custom validation. Currently unused — TOML library validates enums via Codable.
1411
case invalidValue(key: String, value: String, validOptions: [String])
1512
case readError(path: String, underlying: Error)
16-
/// TOML support not available (Linux Homebrew builds)
17-
case tomlNotAvailable
1813

1914
var description: String {
2015
switch self {
@@ -31,9 +26,6 @@ enum ConfigError: Error, CustomStringConvertible {
3126
"Invalid value '\(value)' for '\(key)'. Valid options: \(validOptions.joined(separator: ", "))"
3227
case .readError(let path, let underlying):
3328
return "Failed to read '\(path)': \(underlying.localizedDescription)"
34-
case .tomlNotAvailable:
35-
return
36-
"Configuration files are not supported in this build. Use CLI flags instead."
3729
}
3830
}
3931
}
@@ -61,31 +53,23 @@ struct ConfigLoader {
6153
/// - Returns: Configuration if found, nil if no config file exists (and explicitPath is nil)
6254
/// - Throws: ConfigError for parsing/validation errors
6355
func loadConfig(explicitPath: String?) throws -> Configuration? {
64-
#if canImport(TOML)
65-
let path: String
66-
67-
if let explicit = explicitPath {
68-
// Explicit path must exist
69-
guard fileSystem.fileExists(atPath: explicit) else {
70-
throw ConfigError.fileNotFound(path: explicit)
71-
}
72-
path = explicit
73-
} else {
74-
// Search order: CWD, then user config
75-
guard let foundPath = findConfigFile() else {
76-
return nil // No config file, use defaults
77-
}
78-
path = foundPath
79-
}
56+
let path: String
8057

81-
return try parseConfigFile(at: path)
82-
#else
83-
// TOML support not available (Homebrew Linux builds)
84-
if explicitPath != nil {
85-
throw ConfigError.tomlNotAvailable
58+
if let explicit = explicitPath {
59+
// Explicit path must exist
60+
guard fileSystem.fileExists(atPath: explicit) else {
61+
throw ConfigError.fileNotFound(path: explicit)
62+
}
63+
path = explicit
64+
} else {
65+
// Search order: CWD, then user config
66+
guard let foundPath = findConfigFile() else {
67+
return nil // No config file, use defaults
8668
}
87-
return nil // Silently use CLI defaults when no explicit path requested
88-
#endif
69+
path = foundPath
70+
}
71+
72+
return try parseConfigFile(at: path)
8973
}
9074

9175
/// Generates a template configuration file content with all options commented out
@@ -132,81 +116,87 @@ struct ConfigLoader {
132116

133117
// MARK: - Private Methods
134118

135-
#if canImport(TOML)
136-
private func findConfigFile() -> String? {
137-
// 1. Check current working directory
138-
let cwdPath = URL(fileURLWithPath: fileSystem.currentDirectoryPath)
139-
.appendingPathComponent(Self.configFileName).path
140-
if fileSystem.fileExists(atPath: cwdPath) {
141-
return cwdPath
142-
}
143-
144-
// 2. Check user config directory
145-
let userPath = fileSystem.homeDirectoryForCurrentUser
146-
.appendingPathComponent(Self.userConfigPath).path
147-
if fileSystem.fileExists(atPath: userPath) {
148-
return userPath
149-
}
119+
private func findConfigFile() -> String? {
120+
// 1. Check current working directory
121+
let cwdPath = URL(fileURLWithPath: fileSystem.currentDirectoryPath)
122+
.appendingPathComponent(Self.configFileName).path
123+
if fileSystem.fileExists(atPath: cwdPath) {
124+
return cwdPath
125+
}
150126

151-
return nil
127+
// 2. Check user config directory
128+
let userPath = fileSystem.homeDirectoryForCurrentUser
129+
.appendingPathComponent(Self.userConfigPath).path
130+
if fileSystem.fileExists(atPath: userPath) {
131+
return userPath
152132
}
153133

154-
private func parseConfigFile(at path: String) throws -> Configuration {
155-
let contents: String
156-
do {
157-
contents = try fileSystem.contentsOfFile(atPath: path)
158-
} catch {
159-
throw ConfigError.readError(path: path, underlying: error)
160-
}
134+
return nil
135+
}
136+
137+
private func parseConfigFile(at path: String) throws -> Configuration {
138+
let contents: String
139+
do {
140+
contents = try fileSystem.contentsOfFile(atPath: path)
141+
} catch {
142+
throw ConfigError.readError(path: path, underlying: error)
143+
}
161144

162-
let decoder = TOMLDecoder()
145+
let decoder = TOMLDecoder()
163146

164-
do {
165-
return try decoder.decode(Configuration.self, from: contents)
166-
} catch let error as DecodingError {
167-
throw mapDecodingError(error)
168-
} catch {
169-
// Generic TOML parsing error
170-
throw ConfigError.syntaxError(line: 0, column: 0, message: error.localizedDescription)
147+
do {
148+
return try decoder.decode(Configuration.self, from: contents)
149+
} catch let error as TOMLDecodingError {
150+
// Handle swift-toml 2.0 specific errors with line/column info
151+
switch error {
152+
case .invalidSyntax(let line, let column, let message):
153+
throw ConfigError.syntaxError(line: line, column: column, message: message)
154+
default:
155+
throw ConfigError.syntaxError(line: 0, column: 0, message: error.description)
171156
}
157+
} catch let error as DecodingError {
158+
throw mapDecodingError(error)
159+
} catch {
160+
// Generic parsing error
161+
throw ConfigError.syntaxError(line: 0, column: 0, message: error.localizedDescription)
172162
}
163+
}
173164

174-
private func mapDecodingError(_ error: DecodingError) -> ConfigError {
175-
switch error {
176-
case .typeMismatch(let type, let context):
177-
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
178-
return .syntaxError(
179-
line: 0,
180-
column: 0,
181-
message: "Type mismatch at '\(path)': expected \(type)"
182-
)
183-
case .valueNotFound(let type, let context):
184-
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
185-
return .syntaxError(
186-
line: 0,
187-
column: 0,
188-
message: "Missing value at '\(path)': expected \(type)"
189-
)
190-
case .keyNotFound(let key, let context):
191-
let path =
192-
(context.codingPath.map { $0.stringValue } + [key.stringValue]).joined(
193-
separator: "."
194-
)
195-
return .syntaxError(
196-
line: 0,
197-
column: 0,
198-
message: "Missing key: '\(path)'"
165+
private func mapDecodingError(_ error: DecodingError) -> ConfigError {
166+
switch error {
167+
case .typeMismatch(let type, let context):
168+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
169+
return .syntaxError(
170+
line: 0,
171+
column: 0,
172+
message: "Type mismatch at '\(path)': expected \(type)"
173+
)
174+
case .valueNotFound(let type, let context):
175+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
176+
return .syntaxError(
177+
line: 0,
178+
column: 0,
179+
message: "Missing value at '\(path)': expected \(type)"
180+
)
181+
case .keyNotFound(let key, let context):
182+
let path =
183+
(context.codingPath.map { $0.stringValue } + [key.stringValue]).joined(
184+
separator: "."
199185
)
200-
case .dataCorrupted(let context):
201-
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
202-
let message =
203-
path.isEmpty
204-
? context.debugDescription
205-
: "Invalid data at '\(path)': \(context.debugDescription)"
206-
return .syntaxError(line: 0, column: 0, message: message)
207-
@unknown default:
208-
return .syntaxError(line: 0, column: 0, message: error.localizedDescription)
209-
}
186+
return .syntaxError(
187+
line: 0,
188+
column: 0,
189+
message: "Missing key: '\(path)'"
190+
)
191+
case .dataCorrupted(let context):
192+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
193+
let message =
194+
path.isEmpty
195+
? context.debugDescription
196+
: "Invalid data at '\(path)': \(context.debugDescription)"
197+
return .syntaxError(line: 0, column: 0, message: message)
198+
@unknown default:
199+
return .syntaxError(line: 0, column: 0, message: error.localizedDescription)
210200
}
211-
#endif
201+
}
212202
}

Tests/ConfigurationTests.swift

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,10 @@ final class ConfigLoaderTests: XCTestCase {
324324
XCTFail("Expected ConfigError, got \(error)")
325325
return
326326
}
327-
if case .syntaxError = configError {
328-
// Success
327+
if case .syntaxError(let line, let column, _) = configError {
328+
// swift-toml 2.0 provides line/column info for syntax errors
329+
XCTAssertEqual(line, 1, "Expected error on line 1")
330+
XCTAssertGreaterThan(column, 0, "Expected column > 0")
329331
} else {
330332
XCTFail("Expected syntaxError error, got \(configError)")
331333
}
@@ -917,12 +919,4 @@ final class ConfigErrorTests: XCTestCase {
917919
let error = ConfigError.readError(path: "/test/config.toml", underlying: underlying)
918920
XCTAssertTrue(error.description.contains("Failed to read '/test/config.toml'"))
919921
}
920-
921-
func testTomlNotAvailableDescription() {
922-
let error = ConfigError.tomlNotAvailable
923-
XCTAssertEqual(
924-
error.description,
925-
"Configuration files are not supported in this build. Use CLI flags instead."
926-
)
927-
}
928922
}

0 commit comments

Comments
 (0)