Skip to content

Commit 4526940

Browse files
leogdionclaude
andcommitted
feat: add SPM analysis and validation with task completion
- Implement SPMAnalyzer for package data parsing - Add SPMValidator with structure, version, and platform validation - Mark task 1 and subtasks as complete - Add comprehensive analysis and validation capabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1425079 commit 4526940

File tree

4 files changed

+878
-5
lines changed

4 files changed

+878
-5
lines changed

.taskmaster/tasks/tasks.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"testStrategy": "Unit tests for JSON parsing with fixture files containing sample dump-package outputs, integration tests for SPMExecutor with real swift package commands on test packages, validate error handling for malformed JSON and failed processes",
1010
"priority": "high",
1111
"dependencies": [],
12-
"status": "in-progress",
12+
"status": "done",
1313
"subtasks": [
1414
{
1515
"id": 1,
@@ -39,7 +39,7 @@
3939
"1.1"
4040
],
4141
"details": "Implement ProcessRunner in Sources/SwiftPackageManagerKit/Utilities/ with async execute method that: creates and configures Process instances, sets up pipes for stdout/stderr capture, handles process termination with configurable timeout (default 30 seconds), returns ProcessResult struct containing exitCode, stdout, stderr strings, throws ProcessError for failures (timeout, non-zero exit, launch failure). Use Task for timeout handling and process cancellation support.",
42-
"status": "pending",
42+
"status": "done",
4343
"testStrategy": "Test with various shell commands (echo, ls, false), verify timeout handling with sleep command, test error cases and cancellation"
4444
},
4545
{
@@ -50,7 +50,7 @@
5050
"1.3"
5151
],
5252
"details": "Implement SPMExecutor in Sources/SwiftPackageManagerKit/Execution/ with methods: dumpPackage(at packagePath: URL) async throws -> Data (executes 'swift package dump-package' and returns JSON data), resolvePackage(at packagePath: URL) async throws (executes 'swift package resolve'), buildPackage(at packagePath: URL) async throws (executes 'swift package build'). Use ProcessRunner for command execution, validate swift binary availability, handle working directory changes, provide detailed error messages for common failures.",
53-
"status": "pending",
53+
"status": "done",
5454
"testStrategy": "Integration test with real swift package commands on test fixtures, mock ProcessRunner for unit tests, verify error handling for invalid packages"
5555
},
5656
{
@@ -62,7 +62,7 @@
6262
"1.4"
6363
],
6464
"details": "Implement SPMAnalyzer in Sources/SwiftPackageManagerKit/Analysis/ with analyzePackage(data: Data) throws -> SPMPackageInfo method using JSONDecoder with proper error handling for malformed JSON. Create SPMValidator in Sources/SwiftPackageManagerKit/Validation/ with methods: validatePackageStructure(package: SPMPackageInfo) -> [ValidationIssue] (checks for missing products, orphaned targets, circular dependencies), validateVersionRequirements(dependencies: [SPMDependency]) -> [ValidationIssue] (validates semantic version ranges), validatePlatforms(platforms: [SPMPlatform]) -> [ValidationIssue] (ensures platform versions are valid).",
65-
"status": "pending",
65+
"status": "done",
6666
"testStrategy": "Test analyzer with various dump-package outputs including edge cases, test validator identifies common issues like circular dependencies and invalid versions"
6767
}
6868
]
@@ -679,7 +679,7 @@
679679
],
680680
"metadata": {
681681
"created": "2025-08-14T21:02:02.251Z",
682-
"updated": "2025-08-15T18:41:59.810Z",
682+
"updated": "2025-08-15T19:28:22.379Z",
683683
"description": "Tasks for master context"
684684
}
685685
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import Foundation
2+
3+
/// Errors that can occur during SPM analysis
4+
public enum SPMAnalysisError: Error, LocalizedError {
5+
case invalidJSON(String)
6+
case malformedPackageStructure(String)
7+
case unsupportedFormat(String)
8+
9+
public var errorDescription: String? {
10+
switch self {
11+
case .invalidJSON(let details):
12+
return "Invalid JSON format: \(details)"
13+
case .malformedPackageStructure(let details):
14+
return "Malformed package structure: \(details)"
15+
case .unsupportedFormat(let details):
16+
return "Unsupported format: \(details)"
17+
}
18+
}
19+
}
20+
21+
/// Analyzer for parsing Swift Package Manager JSON output
22+
public struct SPMAnalyzer: Sendable {
23+
24+
/// JSON decoder with custom configuration for SPM data
25+
private let decoder: JSONDecoder
26+
27+
/// Initialize with custom JSON decoder configuration
28+
/// - Parameter decoder: Custom JSONDecoder (uses default if nil)
29+
public init(decoder: JSONDecoder? = nil) {
30+
self.decoder = decoder ?? {
31+
let decoder = JSONDecoder()
32+
// Configure decoder for SPM JSON format
33+
decoder.dateDecodingStrategy = .iso8601
34+
return decoder
35+
}()
36+
}
37+
38+
/// Analyze package JSON data and return parsed package information
39+
/// - Parameter data: Raw JSON data from swift package dump-package
40+
/// - Returns: Parsed SPMPackageInfo
41+
/// - Throws: SPMAnalysisError for parsing failures
42+
public func analyzePackage(data: Data) throws -> SPMPackageInfo {
43+
guard !data.isEmpty else {
44+
throw SPMAnalysisError.invalidJSON("Empty data provided")
45+
}
46+
47+
do {
48+
let packageInfo = try decoder.decode(SPMPackageInfo.self, from: data)
49+
50+
// Basic validation to ensure we have essential fields
51+
try validateBasicStructure(packageInfo)
52+
53+
return packageInfo
54+
55+
} catch let decodingError as DecodingError {
56+
throw SPMAnalysisError.invalidJSON(decodingError.localizedDescription)
57+
} catch let error as SPMAnalysisError {
58+
throw error
59+
} catch {
60+
throw SPMAnalysisError.malformedPackageStructure(error.localizedDescription)
61+
}
62+
}
63+
64+
/// Analyze package from JSON string
65+
/// - Parameter jsonString: JSON string from swift package dump-package
66+
/// - Returns: Parsed SPMPackageInfo
67+
/// - Throws: SPMAnalysisError for parsing failures
68+
public func analyzePackage(jsonString: String) throws -> SPMPackageInfo {
69+
guard let data = jsonString.data(using: .utf8) else {
70+
throw SPMAnalysisError.invalidJSON("Could not convert string to UTF-8 data")
71+
}
72+
return try analyzePackage(data: data)
73+
}
74+
75+
/// Analyze package directly from file path
76+
/// - Parameter filePath: Path to JSON file containing dump-package output
77+
/// - Returns: Parsed SPMPackageInfo
78+
/// - Throws: SPMAnalysisError for parsing failures
79+
public func analyzePackage(filePath: String) throws -> SPMPackageInfo {
80+
do {
81+
let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
82+
return try analyzePackage(data: data)
83+
} catch {
84+
throw SPMAnalysisError.invalidJSON("Could not read file at \(filePath): \(error.localizedDescription)")
85+
}
86+
}
87+
88+
/// Extract basic package summary from analyzed data
89+
/// - Parameter packageInfo: Parsed package information
90+
/// - Returns: Dictionary with key package metrics
91+
public func extractSummary(from packageInfo: SPMPackageInfo) -> [String: Any] {
92+
return [
93+
"name": packageInfo.name,
94+
"toolsVersion": packageInfo.toolsVersion.version,
95+
"platformCount": packageInfo.platforms.count,
96+
"productCount": packageInfo.products.count,
97+
"targetCount": packageInfo.targets.count,
98+
"dependencyCount": packageInfo.dependencies.count,
99+
"libraryProducts": packageInfo.libraryProducts.count,
100+
"executableProducts": packageInfo.executableProducts.count,
101+
"regularTargets": packageInfo.targets.filter { $0.type == .regular }.count,
102+
"testTargets": packageInfo.targets.filter { $0.type == .test }.count,
103+
"executableTargets": packageInfo.targets.filter { $0.type == .executable }.count
104+
]
105+
}
106+
107+
/// Perform basic validation on parsed package structure
108+
/// - Parameter packageInfo: Parsed package information to validate
109+
/// - Throws: SPMAnalysisError if basic structure is invalid
110+
private func validateBasicStructure(_ packageInfo: SPMPackageInfo) throws {
111+
// Validate package name is not empty
112+
guard !packageInfo.name.isEmpty else {
113+
throw SPMAnalysisError.malformedPackageStructure("Package name cannot be empty")
114+
}
115+
116+
// Validate tools version is present
117+
guard !packageInfo.toolsVersion.version.isEmpty else {
118+
throw SPMAnalysisError.malformedPackageStructure("Tools version cannot be empty")
119+
}
120+
121+
// Validate that products reference existing targets
122+
let targetNames = Set(packageInfo.targets.map { $0.name })
123+
for product in packageInfo.products {
124+
for targetName in product.targets {
125+
guard targetNames.contains(targetName) else {
126+
throw SPMAnalysisError.malformedPackageStructure(
127+
"Product '\(product.name)' references non-existent target '\(targetName)'"
128+
)
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
// MARK: - Convenience Extensions
136+
137+
extension SPMAnalyzer {
138+
139+
/// Default shared analyzer instance
140+
public static let shared = SPMAnalyzer()
141+
142+
/// Quick analysis of package data with error handling
143+
/// - Parameter data: JSON data from swift package dump-package
144+
/// - Returns: Result containing either SPMPackageInfo or SPMAnalysisError
145+
public static func analyze(_ data: Data) -> Result<SPMPackageInfo, SPMAnalysisError> {
146+
do {
147+
let packageInfo = try shared.analyzePackage(data: data)
148+
return .success(packageInfo)
149+
} catch let error as SPMAnalysisError {
150+
return .failure(error)
151+
} catch {
152+
return .failure(.malformedPackageStructure(error.localizedDescription))
153+
}
154+
}
155+
156+
/// Quick analysis of package JSON string with error handling
157+
/// - Parameter jsonString: JSON string from swift package dump-package
158+
/// - Returns: Result containing either SPMPackageInfo or SPMAnalysisError
159+
public static func analyze(_ jsonString: String) -> Result<SPMPackageInfo, SPMAnalysisError> {
160+
do {
161+
let packageInfo = try shared.analyzePackage(jsonString: jsonString)
162+
return .success(packageInfo)
163+
} catch let error as SPMAnalysisError {
164+
return .failure(error)
165+
} catch {
166+
return .failure(.malformedPackageStructure(error.localizedDescription))
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)