@@ -6,18 +6,18 @@ import Foundation
6
6
class Xcodeproj {
7
7
let projectURL : URL // points to the "<projectDirectory>/<projectName>.xcodeproj/project.pbxproj" file
8
8
private let pbxproj : PBXProjFile
9
-
9
+
10
10
/// Semantic type for strings that correspond to an object' UUID in the `pbxproj` file
11
11
typealias ObjectUUID = String
12
-
12
+
13
13
/// Builds an `Xcodeproj` instance by parsing the `.xcodeproj` or `.pbxproj` file at the provided URL.
14
14
init ( url: URL ) throws {
15
15
projectURL = url. pathExtension == " xcodeproj " ? URL ( fileURLWithPath: " project.pbxproj " , relativeTo: url) : url
16
16
let data = try Data ( contentsOf: projectURL)
17
17
let decoder = PropertyListDecoder ( )
18
18
pbxproj = try decoder. decode ( PBXProjFile . self, from: data)
19
19
}
20
-
20
+
21
21
/// An internal mapping listing the parent ObjectUUID for each ObjectUUID.
22
22
/// - Built by recursing top-to-bottom in the various `PBXGroup` objects of the project to visit all the children objects,
23
23
/// and storing which parent object they belong to.
@@ -43,14 +43,14 @@ extension Xcodeproj {
43
43
convenience init ( path: String ) throws {
44
44
try self . init ( url: URL ( fileURLWithPath: path) )
45
45
}
46
-
47
- /// The directory where the `.xcodeproj` resides.
46
+
47
+ /// The directory where the `.xcodeproj` resides.
48
48
var projectDirectory : URL { projectURL. deletingLastPathComponent ( ) . deletingLastPathComponent ( ) }
49
49
/// The list of `PBXNativeTarget` targets in the project. Convenience getter for `PBXProjFile.nativeTargets`
50
50
var nativeTargets : [ PBXNativeTarget ] { pbxproj. nativeTargets }
51
51
/// The list of `PBXBuildFile` files a given `PBXNativeTarget` will build. Convenience getter for `PBXProjFile.buildFiles(for:)`
52
52
func buildFiles( for target: PBXNativeTarget ) -> [ PBXBuildFile ] { pbxproj. buildFiles ( for: target) }
53
-
53
+
54
54
/// Finds the full path / URL of a `PBXBuildFile` based on the groups it belongs to and their `sourceTree` attribute
55
55
func resolveURL( to buildFile: PBXBuildFile ) throws -> URL ? {
56
56
if let fileRefID = buildFile. fileRef, let fileRefObject = try ? self . pbxproj. object ( id: fileRefID) as PBXFileReference {
@@ -62,11 +62,11 @@ extension Xcodeproj {
62
62
return nil // just skip those (but don't throw — those are valid use cases in any pbxproj, just ones we don't care about)
63
63
}
64
64
}
65
-
65
+
66
66
/// Finds the full path / URL of a PBXReference (`PBXFileReference` of `PBXGroup`) based on the groups it belongs to and their `sourceTree` attribute
67
67
private func resolveURL< T: PBXReference > ( objectUUID: ObjectUUID , object: T ) throws -> URL ? {
68
68
if objectUUID == self . pbxproj. rootProject. mainGroup { return URL ( fileURLWithPath: " . " , relativeTo: projectDirectory) }
69
-
69
+
70
70
switch object. sourceTree {
71
71
case . absolute:
72
72
guard let path = object. path else { throw ProjectInconsistencyError . incorrectAbsolutePath ( id: objectUUID) }
@@ -89,7 +89,7 @@ extension Xcodeproj {
89
89
90
90
/// "Parent" type for all the PBX... types of objects encountered in a pbxproj
91
91
protocol PBXObject : Decodable {
92
- static var isa : String { get }
92
+ static var isa : String { get }
93
93
}
94
94
extension PBXObject {
95
95
static var isa : String { String ( describing: self ) }
@@ -110,7 +110,7 @@ extension Xcodeproj {
110
110
case unexpectedObjectType( id: ObjectUUID , expectedType: Any . Type , found: PBXObject )
111
111
case incorrectAbsolutePath( id: ObjectUUID )
112
112
case orphanObject( id: ObjectUUID , object: PBXObject )
113
-
113
+
114
114
var description : String {
115
115
switch self {
116
116
case . objectNotFound( id: let id) :
@@ -129,9 +129,9 @@ extension Xcodeproj {
129
129
struct PBXProjFile : Decodable {
130
130
let rootObject : ObjectUUID
131
131
let objects : [ String : PBXObjectWrapper ]
132
-
132
+
133
133
// Convenience methods
134
-
134
+
135
135
/// Returns the `PBXObject` instance with the given `ObjectUUID`, by looking it up in the list of `objects` registered in the project.
136
136
func object< T: PBXObject > ( id: ObjectUUID ) throws -> T {
137
137
guard let wrapped = objects [ id] else { throw ProjectInconsistencyError . objectNotFound ( id: id) }
@@ -140,35 +140,35 @@ extension Xcodeproj {
140
140
}
141
141
return obj
142
142
}
143
-
143
+
144
144
/// Returns the `PBXObject` instance with the given `ObjectUUID`, by looking it up in the list of `objects` registered in the project.
145
145
func object< T: PBXObject > ( id: ObjectUUID ) -> T ? {
146
146
try ? object ( id: id) as T
147
147
}
148
-
148
+
149
149
/// The `PBXProject` corresponding to the `rootObject` of the project file.
150
150
var rootProject : PBXProject { try ! object ( id: rootObject) }
151
-
151
+
152
152
/// The `PBXGroup` corresponding to the main groop serving as root for the whole hierarchy of files and groups in the project.
153
153
var mainGroup : PBXGroup { try ! object ( id: rootProject. mainGroup) }
154
-
154
+
155
155
/// The list of `PBXNativeTarget` targets found in the project.
156
156
var nativeTargets : [ PBXNativeTarget ] { rootProject. targets. compactMap ( object ( id: ) ) }
157
-
157
+
158
158
/// The list of `PBXBuildFile` build file references included in a given target.
159
159
func buildFiles( for target: PBXNativeTarget ) -> [ PBXBuildFile ] {
160
160
guard let sourceBuildPhase: PBXSourcesBuildPhase = target. buildPhases. lazy. compactMap ( object ( id: ) ) . first else { return [ ] }
161
161
return sourceBuildPhase. files. compactMap ( object ( id: ) ) as [ PBXBuildFile ]
162
162
}
163
163
}
164
-
164
+
165
165
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
166
166
/// Represents the root project object.
167
167
struct PBXProject : PBXObject {
168
168
let mainGroup : ObjectUUID
169
169
let targets : [ ObjectUUID ]
170
170
}
171
-
171
+
172
172
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
173
173
/// Represents a native target (i.e. a target building an app, app extension, bundle...).
174
174
/// - note: Does not represent other types of targets like `PBXAggregateTarget`, only native ones.
@@ -186,20 +186,20 @@ extension Xcodeproj {
186
186
case framework = " com.apple.product-type.framework "
187
187
}
188
188
}
189
-
189
+
190
190
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
191
191
/// Represents a "Compile Sources" build phase containing a list of files to compile.
192
192
/// - note: Does not represent other types of Build Phases that could exist in the project, only "Compile Sources" one
193
193
struct PBXSourcesBuildPhase : PBXObject {
194
194
let files : [ ObjectUUID ]
195
195
}
196
-
196
+
197
197
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
198
198
/// Represents a single build file in a `PBXSourcesBuildPhase` build phase.
199
199
struct PBXBuildFile : PBXObject {
200
200
let fileRef : ObjectUUID ?
201
201
}
202
-
202
+
203
203
/// This type is used to indicate what a file reference in the project is actually relative to
204
204
enum SourceTree : String , Decodable , CustomStringConvertible {
205
205
case absolute = " <absolute> "
@@ -210,15 +210,15 @@ extension Xcodeproj {
210
210
case sdkDir = " SDKROOT "
211
211
var description : String { rawValue }
212
212
}
213
-
213
+
214
214
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
215
215
/// Represents a reference to a file contained in the project tree.
216
216
struct PBXFileReference : PBXReference {
217
217
let name : String ?
218
218
let path : String ?
219
219
let sourceTree : SourceTree
220
220
}
221
-
221
+
222
222
/// One of the many `PBXObject` types encountered in the `.pbxproj` file format.
223
223
/// Represents a group (aka "folder") contained in the project tree.
224
224
struct PBXGroup : PBXReference {
@@ -227,17 +227,17 @@ extension Xcodeproj {
227
227
let sourceTree : SourceTree
228
228
let children : [ ObjectUUID ]
229
229
}
230
-
230
+
231
231
/// Fallback type for any unknown `PBXObject` type.
232
232
struct UnknownPBXObject : PBXObject {
233
233
let isa : String
234
234
}
235
-
235
+
236
236
/// Wrapper helper to decode any `PBXObject` based on the value of their `isa` field
237
237
@propertyWrapper
238
238
struct PBXObjectWrapper : Decodable , CustomDebugStringConvertible {
239
239
let wrappedValue : PBXObject
240
-
240
+
241
241
static let knownTypes : [ PBXObject . Type ] = [
242
242
PBXProject . self,
243
243
PBXGroup . self,
@@ -246,14 +246,14 @@ extension Xcodeproj {
246
246
PBXSourcesBuildPhase . self,
247
247
PBXBuildFile . self
248
248
]
249
-
249
+
250
250
init ( from decoder: Decoder ) throws {
251
251
let untypedObject = try UnknownPBXObject ( from: decoder)
252
252
if let objectType = Self . knownTypes. first ( where: { $0. isa == untypedObject. isa } ) {
253
253
self . wrappedValue = try objectType. init ( from: decoder)
254
- } else {
254
+ } else {
255
255
self . wrappedValue = untypedObject
256
- }
256
+ }
257
257
}
258
258
var debugDescription : String { String ( describing: wrappedValue) }
259
259
}
@@ -276,7 +276,7 @@ func lint(fileAt url: URL, targetName: String) throws -> LintResult {
276
276
lineNo += 1
277
277
guard line. range ( of: " \\ s*// " , options: . regularExpression) == nil else { return } // Skip commented lines
278
278
guard let range = line. range ( of: " NSLocalizedString " ) else { return }
279
-
279
+
280
280
let colNo = line. distance ( from: line. startIndex, to: range. lowerBound)
281
281
let message = " Use `AppLocalizedString` instead of `NSLocalizedString` in source files that are used in the ` \( targetName) ` extension target. See paNNhX-nP-p2 for more info. "
282
282
print ( " \( url. path) : \( lineNo) : \( colNo) : error: \( message) " )
@@ -294,7 +294,7 @@ let args = CommandLine.arguments.dropFirst()
294
294
guard let projectPath = args. first, !projectPath. isEmpty else { print ( " You must provide the path to the xcodeproj as first argument. " ) ; exit ( 1 ) }
295
295
do {
296
296
let project = try Xcodeproj ( path: projectPath)
297
-
297
+
298
298
// 2nd arg (optional) = name of target to lint
299
299
let targetsToLint : [ Xcodeproj . PBXNativeTarget ]
300
300
if let targetName = args. dropFirst ( ) . first, !targetName. isEmpty {
304
304
print ( " Linting all app extension targets " )
305
305
targetsToLint = project. nativeTargets. filter { $0. knownProductType == . appExtension }
306
306
}
307
-
307
+
308
308
// Lint each requested target
309
309
var violationsFound = 0
310
310
for target in targetsToLint {
0 commit comments