Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` #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
Expand Down
12 changes: 11 additions & 1 deletion Sources/ProjectSpec/SpecLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.sorted(by: { $0.key < $1.key }) {
if let targetDict = value as? [String: Any] {
for key in targetDict.keys.sorted() where !Target.recognizedKeys.contains(key) {
errors.append(.unrecognizedTargetKey(target: targetName, key: key))
}
}
}
}

if !errors.isEmpty {
throw SpecValidationError(errors: errors)
Expand Down
4 changes: 4 additions & 0 deletions Sources/ProjectSpec/SpecValidationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -116,6 +117,9 @@ 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):
let validKeys = Target.recognizedKeys.sorted().joined(separator: ", ")
return "Target \(target.quoted) has unrecognized key \(key.quoted). Valid target keys are: \(validKeys)"
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions Sources/ProjectSpec/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,23 @@ extension LegacyTarget: JSONEncodable {
}
}

extension Target {

public static let recognizedKeys: Set<String> = [
"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 {
Expand Down
52 changes: 52 additions & 0 deletions Tests/ProjectSpecTests/SpecLoadingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,58 @@ 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)")
}
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)")
}
}
}

$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)")
}
}
}
}

Expand Down
Loading