@@ -92,49 +92,21 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
92
92
self . init ( validate: validate, moduleDependencies: settings. moduleDependencies, fixItContext: fixItContext)
93
93
}
94
94
95
- /// Compute missing module dependencies from Clang imports.
96
- ///
97
- /// The compiler tracing information does not provide the import locations or whether they are public imports
98
- /// (which depends on whether the import is in an installed header file).
99
- /// If `files` is nil, the current toolchain does support the feature to trace imports.
100
- public func computeMissingDependencies( files: [ Path ] ? ) -> [ ( ModuleDependency , importLocations: [ Diagnostic . Location ] ) ] ? {
101
- guard validate != . no else { return [ ] }
102
- guard let files else {
103
- return nil
104
- }
105
-
106
- // The following is a provisional/incomplete mechanism for resolving a module dependency from a file path.
107
- // For now, just grab the framework name and assume there is a module with the same name.
108
- func findFrameworkName( _ file: Path ) -> String ? {
109
- if file. fileExtension == " framework " {
110
- return file. basenameWithoutSuffix
111
- }
112
- return file. dirname. isEmpty || file. dirname. isRoot ? nil : findFrameworkName ( file. dirname)
113
- }
114
-
115
- let moduleDependencyNames = moduleDependencies. map { $0. name }
116
- let fileNames = files. compactMap { findFrameworkName ( $0) }
117
- let missingDeps = Set ( fileNames. filter {
118
- return !moduleDependencyNames. contains ( $0)
119
- } . map {
120
- ModuleDependency ( name: $0, accessLevel: . Private)
121
- } )
122
-
123
- return missingDeps. map { ( $0, [ ] ) }
124
- }
125
-
126
- /// Compute missing module dependencies from Swift imports.
95
+ /// Compute missing module dependencies.
127
96
///
128
97
/// If `imports` is nil, the current toolchain does not support the features to gather imports.
129
- public func computeMissingDependencies( imports: [ ( ModuleDependency , importLocations: [ Diagnostic . Location ] ) ] ? ) -> [ ( ModuleDependency , importLocations: [ Diagnostic . Location ] ) ] ? {
98
+ public func computeMissingDependencies(
99
+ imports: [ ( ModuleDependency , importLocations: [ Diagnostic . Location ] ) ] ? ,
100
+ fromSwift: Bool
101
+ ) -> [ ( ModuleDependency , importLocations: [ Diagnostic . Location ] ) ] ? {
130
102
guard validate != . no else { return [ ] }
131
103
guard let imports else {
132
104
return nil
133
105
}
134
106
135
107
return imports. filter {
136
108
// ignore module deps without source locations, these are inserted by swift / swift-build and we should treat them as implementation details which we can track without needing the user to declare them
137
- if $0. importLocations. isEmpty { return false }
109
+ if fromSwift && $0. importLocations. isEmpty { return false }
138
110
139
111
// TODO: if the difference is just the access modifier, we emit a new entry, but ultimately our fixit should update the existing entry or emit an error about a conflict
140
112
if moduleDependencies. contains ( $0. 0 ) { return false }
@@ -235,6 +207,166 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
235
207
}
236
208
}
237
209
210
+ public struct HeaderDependency : Hashable , Sendable , SerializableCodable {
211
+ public let name : String
212
+ public let accessLevel : AccessLevel
213
+
214
+ public enum AccessLevel : String , Hashable , Sendable , CaseIterable , Codable , Serializable {
215
+ case Private = " private "
216
+ case Public = " public "
217
+
218
+ public init ( _ string: String ) throws {
219
+ guard let accessLevel = AccessLevel ( rawValue: string) else {
220
+ throw StubError . error ( " unexpected access modifier ' \( string) ', expected one of: \( AccessLevel . allCases. map { $0. rawValue } . joined ( separator: " , " ) ) " )
221
+ }
222
+
223
+ self = accessLevel
224
+ }
225
+ }
226
+
227
+ public init ( name: String , accessLevel: AccessLevel ) {
228
+ self . name = name
229
+ self . accessLevel = accessLevel
230
+ }
231
+
232
+ public init ( entry: String ) throws {
233
+ var it = entry. split ( separator: " " ) . makeIterator ( )
234
+ switch ( it. next ( ) , it. next ( ) , it. next ( ) ) {
235
+ case ( let . some( name) , nil , nil ) :
236
+ self . name = String ( name)
237
+ self . accessLevel = . Private
238
+
239
+ case ( let . some( accessLevel) , let . some( name) , nil ) :
240
+ self . name = String ( name)
241
+ self . accessLevel = try AccessLevel ( String ( accessLevel) )
242
+
243
+ default :
244
+ throw StubError . error ( " expected 1 or 2 space-separated components in: \( entry) " )
245
+ }
246
+ }
247
+
248
+ public var asBuildSettingEntry : String {
249
+ " \( accessLevel == . Private ? " " : " \( accessLevel. rawValue) " ) \( name) "
250
+ }
251
+
252
+ public var asBuildSettingEntryQuotedIfNeeded : String {
253
+ let e = asBuildSettingEntry
254
+ return e. contains ( " " ) ? " \" \( e) \" " : e
255
+ }
256
+ }
257
+
258
+ public struct HeaderDependenciesContext : Sendable , SerializableCodable {
259
+ public var validate : BooleanWarningLevel
260
+ var headerDependencies : [ HeaderDependency ]
261
+ var fixItContext : FixItContext ?
262
+
263
+ init ( validate: BooleanWarningLevel , headerDependencies: [ HeaderDependency ] , fixItContext: FixItContext ? = nil ) {
264
+ self . validate = validate
265
+ self . headerDependencies = headerDependencies
266
+ self . fixItContext = fixItContext
267
+ }
268
+
269
+ public init ? ( settings: Settings ) {
270
+ let validate = settings. globalScope. evaluate ( BuiltinMacros . VALIDATE_HEADER_DEPENDENCIES)
271
+ guard validate != . no else { return nil }
272
+ let fixItContext = HeaderDependenciesContext . FixItContext ( settings: settings)
273
+ self . init ( validate: validate, headerDependencies: settings. headerDependencies, fixItContext: fixItContext)
274
+ }
275
+
276
+ /// Make diagnostics for missing header dependencies.
277
+ ///
278
+ /// The compiler tracing information does not provide the include locations or whether they are public imports
279
+ /// (which depends on whether the import is in an installed header file).
280
+ /// If `includes` is nil, the current toolchain does support the feature to trace imports.
281
+ public func makeDiagnostics( includes: [ Path ] ? ) -> [ Diagnostic ] {
282
+ guard validate != . no else { return [ ] }
283
+ guard let includes else {
284
+ return [ Diagnostic (
285
+ behavior: . warning,
286
+ location: . unknown,
287
+ data: DiagnosticData ( " The current toolchain does not support \( BuiltinMacros . VALIDATE_HEADER_DEPENDENCIES. name) " ) ) ]
288
+ }
289
+
290
+ let headerDependencyNames = headerDependencies. map { $0. name }
291
+ let missingDeps = includes. filter { file in
292
+ return !headerDependencyNames. contains ( where: { file. ends ( with: $0) } )
293
+ } . map {
294
+ // TODO: What if the basename doesn't uniquely identify the header?
295
+ HeaderDependency ( name: $0. basename, accessLevel: . Private)
296
+ }
297
+
298
+ guard !missingDeps. isEmpty else { return [ ] }
299
+
300
+ let behavior : Diagnostic . Behavior = validate == . yesError ? . error : . warning
301
+
302
+ let fixIt = fixItContext? . makeFixIt ( newHeaders: missingDeps)
303
+ let fixIts = fixIt. map { [ $0] } ?? [ ]
304
+
305
+ let message = " Missing entries in \( BuiltinMacros . HEADER_DEPENDENCIES. name) : \( missingDeps. map { $0. asBuildSettingEntryQuotedIfNeeded } . sorted ( ) . joined ( separator: " " ) ) "
306
+
307
+ let location : Diagnostic . Location = fixIt. map {
308
+ Diagnostic . Location. path ( $0. sourceRange. path, line: $0. sourceRange. endLine, column: $0. sourceRange. endColumn)
309
+ } ?? Diagnostic . Location. buildSetting ( BuiltinMacros . HEADER_DEPENDENCIES)
310
+
311
+ return [ Diagnostic (
312
+ behavior: behavior,
313
+ location: location,
314
+ data: DiagnosticData ( message) ,
315
+ fixIts: fixIts) ]
316
+ }
317
+
318
+ struct FixItContext : Sendable , SerializableCodable {
319
+ var sourceRange : Diagnostic . SourceRange
320
+ var modificationStyle : ModificationStyle
321
+
322
+ init ( sourceRange: Diagnostic . SourceRange , modificationStyle: ModificationStyle ) {
323
+ self . sourceRange = sourceRange
324
+ self . modificationStyle = modificationStyle
325
+ }
326
+
327
+ init ? ( settings: Settings ) {
328
+ guard let target = settings. target else { return nil }
329
+ let thisTargetCondition = MacroCondition ( parameter: BuiltinMacros . targetNameCondition, valuePattern: target. name)
330
+
331
+ if let assignment = ( settings. globalScope. table. lookupMacro ( BuiltinMacros . HEADER_DEPENDENCIES) ? . sequence. first {
332
+ $0. location != nil && ( $0. conditions? . conditions == [ thisTargetCondition] || ( $0. conditions? . conditions. isEmpty ?? true ) )
333
+ } ) ,
334
+ let location = assignment. location
335
+ {
336
+ self . init ( sourceRange: . init( path: location. path, startLine: location. endLine, startColumn: location. endColumn, endLine: location. endLine, endColumn: location. endColumn) , modificationStyle: . appendToExistingAssignment)
337
+ }
338
+ else if let path = settings. constructionComponents. targetXcconfigPath {
339
+ self . init ( sourceRange: . init( path: path, startLine: . max, startColumn: . max, endLine: . max, endColumn: . max) , modificationStyle: . insertNewAssignment( targetNameCondition: nil ) )
340
+ }
341
+ else if let path = settings. constructionComponents. projectXcconfigPath {
342
+ self . init ( sourceRange: . init( path: path, startLine: . max, startColumn: . max, endLine: . max, endColumn: . max) , modificationStyle: . insertNewAssignment( targetNameCondition: target. name) )
343
+ }
344
+ else {
345
+ return nil
346
+ }
347
+ }
348
+
349
+ enum ModificationStyle : Sendable , SerializableCodable , Hashable {
350
+ case appendToExistingAssignment
351
+ case insertNewAssignment( targetNameCondition: String ? )
352
+ }
353
+
354
+ func makeFixIt( newHeaders: [ HeaderDependency ] ) -> Diagnostic . FixIt {
355
+ let stringValue = newHeaders. map { $0. asBuildSettingEntryQuotedIfNeeded } . sorted ( ) . joined ( separator: " " )
356
+ let newText : String
357
+ switch modificationStyle {
358
+ case . appendToExistingAssignment:
359
+ newText = " \( stringValue) "
360
+ case . insertNewAssignment( let targetNameCondition) :
361
+ let targetCondition = targetNameCondition. map { " [target= \( $0) ] " } ?? " "
362
+ newText = " \n \( BuiltinMacros . HEADER_DEPENDENCIES. name) \( targetCondition) = $(inherited) \( stringValue) \n "
363
+ }
364
+
365
+ return Diagnostic . FixIt ( sourceRange: sourceRange, newText: newText)
366
+ }
367
+ }
368
+ }
369
+
238
370
public struct DependencyValidationInfo : Hashable , Sendable , Codable {
239
371
public struct Import : Hashable , Sendable , Codable {
240
372
public let dependency : ModuleDependency
@@ -247,7 +379,7 @@ public struct DependencyValidationInfo: Hashable, Sendable, Codable {
247
379
}
248
380
249
381
public enum Payload : Hashable , Sendable , Codable {
250
- case clangDependencies( files : [ String ] )
382
+ case clangDependencies( imports : [ Import ] , includes : [ Path ] )
251
383
case swiftDependencies( imports: [ Import ] )
252
384
case unsupported
253
385
}
0 commit comments