From 5cd2ff76334f2e5fa7b4abefeb4be7fcbb0bf7de Mon Sep 17 00:00:00 2001 From: FawwazLawal <263737229+FawwazLawal@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:48:04 -0700 Subject: [PATCH 1/2] Warn on unrecognized keys in target dictionaries Adds a validation warning when a target dictionary contains keys not in Target.recognizedKeys, helping catch typos like `resources` instead of `sources`. Resolves #1645 --- CHANGELOG.md | 1 + Sources/ProjectSpec/SpecLoader.swift | 12 ++++- Sources/ProjectSpec/SpecValidationError.swift | 3 ++ Sources/ProjectSpec/Target.swift | 17 +++++++ Tests/ProjectSpecTests/SpecLoadingTests.swift | 48 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa70da5..145cba9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next Version ### Added +- Added a warning when a target dictionary contains keys not recognized by `Target.init`, helping catch typos such as `resources` instead of `sources` #1645 - Added `buildArchitectures` to scheme build options and target schemes, to control Xcode's "Override Architectures" scheme setting #1642 @arhxam - Added FAQ documentation on how to add an Xcode capability, such as In-App Purchase #1644 @Hokila - Added `xcode26_3` `projectFormat` option for the Xcode 26.3 project format (`objectVersion = 100`) #1648 @Raymondriter diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift index 56c31a5c..4956a348 100644 --- a/Sources/ProjectSpec/SpecLoader.swift +++ b/Sources/ProjectSpec/SpecLoader.swift @@ -51,7 +51,17 @@ public class SpecLoader { private extension Dictionary where Key == String, Value: Any { func validateWarnings() throws { - let errors: [SpecValidationError.ValidationError] = [] + var errors: [SpecValidationError.ValidationError] = [] + + if let targets = self["targets"] as? [String: Any] { + for (targetName, value) in targets { + if let targetDict = value as? [String: Any] { + for key in targetDict.keys where !Target.recognizedKeys.contains(key) { + errors.append(.unrecognizedTargetKey(target: targetName, key: key)) + } + } + } + } if !errors.isEmpty { throw SpecValidationError(errors: errors) diff --git a/Sources/ProjectSpec/SpecValidationError.swift b/Sources/ProjectSpec/SpecValidationError.swift index 7f16d2ad..ac677373 100644 --- a/Sources/ProjectSpec/SpecValidationError.swift +++ b/Sources/ProjectSpec/SpecValidationError.swift @@ -44,6 +44,7 @@ public struct SpecValidationError: Error, CustomStringConvertible { case invalidPluginPackageReference(plugin: String, package: String) case emptySourcePath(target: String) case invalidProjectFormat(String) + case unrecognizedTargetKey(target: String, key: String) public var description: String { switch self { @@ -116,6 +117,8 @@ public struct SpecValidationError: Error, CustomStringConvertible { case let .invalidProjectFormat(projectFormat): let validValues = ProjectFormat.allCases.map(\.rawValue).joined(separator: ", ") return "Invalid projectFormat \(projectFormat.quoted). Valid values are: \(validValues)" + case let .unrecognizedTargetKey(target, key): + return "Target \(target.quoted) has unrecognized key \(key.quoted)" } } } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 9d94c7d8..5ac877ff 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -273,6 +273,23 @@ extension LegacyTarget: JSONEncodable { } } +extension Target { + + public static let recognizedKeys: Set = [ + "name", "productName", "type", "supportedDestinations", + "isMultiPlatformTarget", "platform", "deploymentTarget", + "settings", "configFiles", "sources", "dependencies", + "buildToolPlugins", "info", "entitlements", + "transitivelyLinkDependencies", "directlyEmbedCarthageDependencies", + "requiresObjCLinking", "preBuildScripts", "prebuildScripts", + "postCompileScripts", "postBuildScripts", "postbuildScripts", + "buildRules", "scheme", "legacy", "attributes", + "onlyCopyFilesOnInstall", "putResourcesBeforeSourcesBuildPhase", + "templates", "templateAttributes", + "platformPrefix", "platformSuffix", + ] +} + extension Target: NamedJSONDictionaryConvertible { public init(name: String, jsonDictionary: JSONDictionary) throws { diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 2efee3ba..7dd20450 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -423,6 +423,54 @@ class SpecLoadingTests: XCTestCase { throw failure("Expected to not throw a validation error. Got: \(error)") } } + + $0.it("warns on unrecognized target keys") { + let dictionary: [String: Any] = [ + "name": "TestUnrecognizedKey", + "targets": [ + "MyApp": [ + "type": "application", + "platform": "iOS", + "resources": ["Resources/**"], + ], + ], + ] + try? Yams.dump(object: dictionary).write(toFile: path.string, atomically: true, encoding: .utf8) + let specLoader = SpecLoader(version: "1.1.0") + _ = try specLoader.loadProject(path: path) + do { + try specLoader.validateProjectDictionaryWarnings() + throw failure("Expected a warning for unrecognized key \"resources\"") + } catch let error as SpecValidationError { + let expected = SpecValidationError.ValidationError.unrecognizedTargetKey(target: "MyApp", key: "resources") + if !error.errors.contains(expected) { + throw failure("Expected warning for unrecognized key \"resources\", got: \(error)") + } + } + } + + $0.it("does not warn on recognized target keys") { + let dictionary: [String: Any] = [ + "name": "TestRecognizedKeys", + "targets": [ + "MyApp": [ + "type": "application", + "platform": "iOS", + "sources": ["Sources"], + "dependencies": [], + "settings": ["SWIFT_VERSION": "5"], + ], + ], + ] + try? Yams.dump(object: dictionary).write(toFile: path.string, atomically: true, encoding: .utf8) + let specLoader = SpecLoader(version: "1.1.0") + _ = try specLoader.loadProject(path: path) + do { + try specLoader.validateProjectDictionaryWarnings() + } catch { + throw failure("Unexpected warning for recognized keys: \(error)") + } + } } } From 3541292fb47b7f9e8050f80414fa025c9da9b5c4 Mon Sep 17 00:00:00 2001 From: FawwazLawal <263737229+FawwazLawal@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:28:18 -0700 Subject: [PATCH 2/2] List the valid target keys in the warning and sort its output --- CHANGELOG.md | 2 +- Sources/ProjectSpec/SpecLoader.swift | 4 ++-- Sources/ProjectSpec/SpecValidationError.swift | 3 ++- Tests/ProjectSpecTests/SpecLoadingTests.swift | 4 ++++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145cba9b..921efe55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Next Version ### Added -- Added a warning when a target dictionary contains keys not recognized by `Target.init`, helping catch typos such as `resources` instead of `sources` #1645 +- Added a warning when a target dictionary contains keys not recognized by `Target.init`, helping catch typos such as `resources` instead of `sources` #1660 @FawwazLawal - Added `buildArchitectures` to scheme build options and target schemes, to control Xcode's "Override Architectures" scheme setting #1642 @arhxam - Added FAQ documentation on how to add an Xcode capability, such as In-App Purchase #1644 @Hokila - Added `xcode26_3` `projectFormat` option for the Xcode 26.3 project format (`objectVersion = 100`) #1648 @Raymondriter diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift index 4956a348..bf155d57 100644 --- a/Sources/ProjectSpec/SpecLoader.swift +++ b/Sources/ProjectSpec/SpecLoader.swift @@ -54,9 +54,9 @@ private extension Dictionary where Key == String, Value: Any { var errors: [SpecValidationError.ValidationError] = [] if let targets = self["targets"] as? [String: Any] { - for (targetName, value) in targets { + for (targetName, value) in targets.sorted(by: { $0.key < $1.key }) { if let targetDict = value as? [String: Any] { - for key in targetDict.keys where !Target.recognizedKeys.contains(key) { + for key in targetDict.keys.sorted() where !Target.recognizedKeys.contains(key) { errors.append(.unrecognizedTargetKey(target: targetName, key: key)) } } diff --git a/Sources/ProjectSpec/SpecValidationError.swift b/Sources/ProjectSpec/SpecValidationError.swift index ac677373..1fce6a6a 100644 --- a/Sources/ProjectSpec/SpecValidationError.swift +++ b/Sources/ProjectSpec/SpecValidationError.swift @@ -118,7 +118,8 @@ public struct SpecValidationError: Error, CustomStringConvertible { let validValues = ProjectFormat.allCases.map(\.rawValue).joined(separator: ", ") return "Invalid projectFormat \(projectFormat.quoted). Valid values are: \(validValues)" case let .unrecognizedTargetKey(target, key): - return "Target \(target.quoted) has unrecognized key \(key.quoted)" + let validKeys = Target.recognizedKeys.sorted().joined(separator: ", ") + return "Target \(target.quoted) has unrecognized key \(key.quoted). Valid target keys are: \(validKeys)" } } } diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 7dd20450..9b13ceec 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -446,6 +446,10 @@ class SpecLoadingTests: XCTestCase { if !error.errors.contains(expected) { throw failure("Expected warning for unrecognized key \"resources\", got: \(error)") } + let message = expected.description + if !message.contains("\"resources\"") || !message.contains("Valid target keys are:") { + throw failure("Expected message to name the key and list valid keys, got: \(message)") + } } }