From 843d967edc0ded6401ebeadd5e1bf6241a0b039d Mon Sep 17 00:00:00 2001 From: Swastik Tripathi Date: Sun, 25 Oct 2026 08:38:05 +0530 Subject: [PATCH] Warn when a target contains an unrecognized YAML key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Target.init(jsonDictionary:)` silently ignores any top-level key that it does not recognise. This makes it very easy to misplace valid fields (e.g. writing `resources:` instead of adding a source entry with `buildPhase: resources`) and get a project that generates successfully but is simply missing resources at runtime, with no diagnostic of any kind. Add `Target.validYAMLKeys` — the exhaustive set of top-level keys that Target parsing understands — and extend `validateProjectDictionaryWarnings` to surface any key outside that set as a `SpecValidationError`. The warning is printed by the `generate` command before the project file is written, giving users an early signal that a key will be ignored. Fixes #1645 Co-Authored-By: Claude Sonnet 4.6 --- Sources/ProjectSpec/SpecLoader.swift | 12 +++++++- Sources/ProjectSpec/SpecValidationError.swift | 3 ++ Sources/ProjectSpec/Target.swift | 14 +++++++++ Tests/ProjectSpecTests/SpecLoadingTests.swift | 30 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift index 56c31a5c..0545b88d 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, targetValue) in targets { + guard let targetDict = targetValue as? JSONDictionary else { continue } + let unknownKeys = Set(targetDict.keys).subtracting(Target.validYAMLKeys) + for key in unknownKeys.sorted() { + errors.append(.unknownTargetKey(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..f5545264 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 unknownTargetKey(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 .unknownTargetKey(target, key): + return "Target \(target.quoted) has an unrecognized key \(key.quoted). This key will be ignored. Check the XcodeGen documentation for valid target keys." } } } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 9d94c7d8..d89cd7ca 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -275,6 +275,20 @@ extension LegacyTarget: JSONEncodable { extension Target: NamedJSONDictionaryConvertible { + /// All YAML keys recognized at the top level of a target definition. + public static let validYAMLKeys: Set = [ + "name", "productName", "type", "platform", "supportedDestinations", + "isMultiPlatformTarget", "deploymentTarget", "settings", "configFiles", + "sources", "dependencies", "buildToolPlugins", "info", "entitlements", + "transitivelyLinkDependencies", "directlyEmbedCarthageDependencies", + "requiresObjCLinking", "preBuildScripts", "prebuildScripts", + "postCompileScripts", "postBuildScripts", "postbuildScripts", + "buildRules", "scheme", "legacy", "attributes", + "onlyCopyFilesOnInstall", "putResourcesBeforeSourcesBuildPhase", + // Template-related keys consumed before Target.init is called. + "templates", "templateAttributes", + ] + public init(name: String, jsonDictionary: JSONDictionary) throws { let resolvedName: String = jsonDictionary.json(atKeyPath: "name") ?? name self.name = resolvedName diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 2efee3ba..ebd257bc 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -423,6 +423,36 @@ class SpecLoadingTests: XCTestCase { throw failure("Expected to not throw a validation error. Got: \(error)") } } + + $0.it("warns about unrecognized target keys") { + let dictionary: [String: Any] = [ + "name": "TestUnknownTargetKey", + "targets": [ + "App": [ + "type": "application", + "platform": "iOS", + "sources": ["Sources/App"], + "resources": ["Resources"], + ], + ], + ] + try? Yams.dump(object: dictionary).write(toFile: path.string, atomically: true, encoding: .utf8) + let specLoader = SpecLoader(version: "1.1.0") + do { + _ = try specLoader.loadProject(path: path) + } catch { + throw failure("loadProject should not throw for unknown keys. Got: \(error)") + } + do { + try specLoader.validateProjectDictionaryWarnings() + throw failure("Expected a validation error for the unknown 'resources' key") + } catch let error as SpecValidationError { + let expected = SpecValidationError.ValidationError.unknownTargetKey(target: "App", key: "resources") + try expect(error.errors.contains(expected)) == true + } catch { + throw failure("Unexpected error type: \(error)") + } + } } }