@@ -92,49 +92,21 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
9292 self . init ( validate: validate, moduleDependencies: settings. moduleDependencies, fixItContext: fixItContext)
9393 }
9494
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.
12796 ///
12897 /// 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 ] ) ] ? {
130102 guard validate != . no else { return [ ] }
131103 guard let imports else {
132104 return nil
133105 }
134106
135107 return imports. filter {
136108 // 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 }
138110
139111 // 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
140112 if moduleDependencies. contains ( $0. 0 ) { return false }
@@ -235,6 +207,166 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
235207 }
236208}
237209
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+
238370public struct DependencyValidationInfo : Hashable , Sendable , Codable {
239371 public struct Import : Hashable , Sendable , Codable {
240372 public let dependency : ModuleDependency
@@ -247,7 +379,7 @@ public struct DependencyValidationInfo: Hashable, Sendable, Codable {
247379 }
248380
249381 public enum Payload : Hashable , Sendable , Codable {
250- case clangDependencies( files : [ String ] )
382+ case clangDependencies( imports : [ Import ] , includes : [ Path ] )
251383 case swiftDependencies( imports: [ Import ] )
252384 case unsupported
253385 }
0 commit comments