check_imports.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/usr/bin/swift
  2. /*
  3. * Copyright 2020 Google LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. // Utility script for verifying `import` and `include` syntax. This ensures a
  18. // consistent style as well as functionality across multiple package managers.
  19. // For more context, see https://github.com/firebase/firebase-ios-sdk/blob/main/HeadersImports.md.
  20. import Foundation
  21. // Skip these directories. Imports should only be repo-relative in libraries
  22. // and unit tests.
  23. let skipDirPatterns = ["/Sample/", "/Pods/",
  24. "FirebaseDynamicLinks/Tests/Integration",
  25. "FirebaseInAppMessaging/Tests/Integration/",
  26. "FirebaseAuth/",
  27. // TODO: Turn Combine back on without Auth includes.
  28. "FirebaseCombineSwift/Tests/Unit/FirebaseCombine-unit-Bridging-Header.h",
  29. "SymbolCollisionTest/", "/gen/",
  30. "IntegrationTesting/CocoapodsIntegrationTest/",
  31. "FirebasePerformance/Tests/TestApp/",
  32. "cmake-build-debug/", "build/", "ObjCIntegration/",
  33. "FirebasePerformance/Tests/FIRPerfE2E/"] +
  34. [
  35. "CoreOnly/Sources", // Skip Firebase.h.
  36. "SwiftPMTests", // The SwiftPM tests test module imports.
  37. "IntegrationTesting/ClientApp", // The ClientApp tests module imports.
  38. "FirebaseSessions/Protogen/", // Generated nanopb code with imports
  39. ] +
  40. // The following are temporary skips pending working through a first pass of the repo:
  41. [
  42. "FirebaseDatabase/Sources/third_party/Wrap-leveldb", // Pending SwiftPM for leveldb.
  43. "Example",
  44. "Firestore",
  45. "FirebasePerformance/ProtoSupport/",
  46. ]
  47. // Skip existence test for patterns that start with the following:
  48. let skipImportPatterns = [
  49. "FBLPromise",
  50. "OCMock",
  51. "OCMStubRecorder",
  52. ]
  53. private class ErrorLogger {
  54. var foundError = false
  55. func log(_ message: String) {
  56. print(message)
  57. foundError = true
  58. }
  59. func importLog(_ message: String, _ file: String, _ line: Int) {
  60. log("Import Error: \(file):\(line) \(message)")
  61. }
  62. }
  63. private func checkFile(_ file: String, logger: ErrorLogger, inRepo repoURL: URL,
  64. isSwiftFile: Bool) {
  65. var fileContents = ""
  66. do {
  67. fileContents = try String(contentsOfFile: file, encoding: .utf8)
  68. } catch {
  69. logger.log("Could not read \(file). \(error)")
  70. // Not a source file, give up and return.
  71. return
  72. }
  73. guard !isSwiftFile else {
  74. // Swift specific checks.
  75. fileContents.components(separatedBy: .newlines)
  76. .enumerated() // [(lineNum, line), ...]
  77. .filter { $1.starts(with: "import FirebaseCoreExtension") }
  78. .forEach { lineNum, line in
  79. logger
  80. .importLog(
  81. "Use `@_implementationOnly import FirebaseCoreExtension` when importing `FirebaseCoreExtension`.",
  82. file, lineNum
  83. )
  84. }
  85. return
  86. }
  87. let isPublic = file.range(of: "/Public/") != nil &&
  88. // TODO: Skip legacy GDTCCTLibrary file that isn't Public and should be moved.
  89. // This test is used in the GoogleDataTransport's repo's CI clone of this repo.
  90. file.range(of: "GDTCCTLibrary/Public/GDTCOREvent+GDTCCTSupport.h") == nil
  91. let isPrivate = file.range(of: "/Sources/Private/") != nil ||
  92. // Delete when FirebaseInstallations fixes directory structure.
  93. file.range(of: "Source/Library/Private/FirebaseInstallationsInternal.h") != nil ||
  94. file.range(of: "FirebaseCore/Sources/FIROptionsInternal.h") != nil ||
  95. file.range(of: "FirebaseCore/Extension") != nil
  96. // Treat all files with names finishing on "Test" or "Tests" as files with tests.
  97. let isTestFile = file.contains("Test.m") || file.contains("Tests.m") ||
  98. file.contains("Test.swift") || file.contains("Tests.swift")
  99. let isBridgingHeader = file.contains("Bridging-Header.h")
  100. var inSwiftPackage = false
  101. var inSwiftPackageElse = false
  102. let lines = fileContents.components(separatedBy: .newlines)
  103. var lineNum = 0
  104. nextLine: for rawLine in lines {
  105. let line = rawLine.trimmingCharacters(in: .whitespaces)
  106. lineNum += 1
  107. if line.starts(with: "#if SWIFT_PACKAGE") {
  108. inSwiftPackage = true
  109. } else if inSwiftPackage, line.starts(with: "#else") {
  110. inSwiftPackage = false
  111. inSwiftPackageElse = true
  112. } else if inSwiftPackageElse, line.starts(with: "#endif") {
  113. inSwiftPackageElse = false
  114. } else if inSwiftPackage {
  115. continue
  116. } else if file.contains("FirebaseTestingSupport") {
  117. // Module imports ok in SPM only test infrastructure.
  118. continue
  119. }
  120. // "The #else of a SWIFT_PACKAGE check should only do CocoaPods module-style imports."
  121. if line.starts(with: "#import") || line.starts(with: "#include") {
  122. let importFile = line.components(separatedBy: " ")[1]
  123. if inSwiftPackageElse {
  124. if importFile.first != "<" {
  125. logger
  126. .importLog("Import in SWIFT_PACKAGE #else should start with \"<\".", file, lineNum)
  127. }
  128. continue
  129. }
  130. let importFileRaw = importFile.replacingOccurrences(of: "\"", with: "")
  131. .replacingOccurrences(of: "<", with: "")
  132. .replacingOccurrences(of: ">", with: "")
  133. if importFile.first == "\"" {
  134. // Public Headers should only use simple file names without paths.
  135. if isPublic {
  136. if importFile.contains("/") {
  137. logger.importLog("Public header import should not include \"/\"", file, lineNum)
  138. }
  139. } else if !FileManager.default.fileExists(atPath: repoURL.path + "/" + importFileRaw) {
  140. // Non-public header imports should be repo-relative paths. Unqualified imports are
  141. // allowed in private headers.
  142. if !isPrivate || importFile.contains("/") {
  143. for skip in skipImportPatterns {
  144. if importFileRaw.starts(with: skip) {
  145. continue nextLine
  146. }
  147. }
  148. logger.importLog("Import \(importFileRaw) does not exist.", file, lineNum)
  149. }
  150. }
  151. } else if importFile.first == "<", !isPrivate, !isTestFile, !isBridgingHeader, !isPublic {
  152. // Verify that double quotes are always used for intra-module imports.
  153. if importFileRaw.starts(with: "Firebase"),
  154. // Allow intra-module imports of FirebaseAppCheckInterop.
  155. // TODO: Remove the FirebaseAppCheckInterop exception when it's moved to a separate repo.
  156. importFile.range(of: "FirebaseAppCheckInterop/FirebaseAppCheckInterop.h") == nil {
  157. logger
  158. .importLog("Imports internal to the repo should use double quotes not \"<\"", file,
  159. lineNum)
  160. }
  161. }
  162. }
  163. }
  164. }
  165. private func main() -> Int32 {
  166. let logger = ErrorLogger()
  167. // Search the path upwards to find the root of the firebase-ios-sdk repo.
  168. var url = URL(fileURLWithPath: FileManager().currentDirectoryPath)
  169. while url.path != "/" {
  170. let script = url.appendingPathComponent("scripts/check_imports.swift")
  171. if FileManager.default.fileExists(atPath: script.path) {
  172. break
  173. }
  174. url = url.deletingLastPathComponent()
  175. }
  176. let repoURL = url
  177. guard let contents = try? FileManager.default.contentsOfDirectory(at: repoURL,
  178. includingPropertiesForKeys: nil,
  179. options: [.skipsHiddenFiles])
  180. else {
  181. logger.log("Failed to get repo contents \(repoURL)")
  182. return 1
  183. }
  184. for rootURL in contents {
  185. if !rootURL.hasDirectoryPath {
  186. continue
  187. }
  188. let enumerator = FileManager.default.enumerator(atPath: rootURL.path)
  189. whileLoop: while let file = enumerator?.nextObject() as? String {
  190. if let fType = enumerator?.fileAttributes?[FileAttributeKey.type] as? FileAttributeType,
  191. fType == .typeRegular {
  192. if file.starts(with: ".") {
  193. continue
  194. }
  195. if !(file.hasSuffix(".h") ||
  196. file.hasSuffix(".m") ||
  197. file.hasSuffix(".mm") ||
  198. file.hasSuffix(".c") ||
  199. file.hasSuffix(".swift")) {
  200. continue
  201. }
  202. let fullTransformPath = rootURL.path + "/" + file
  203. for dirPattern in skipDirPatterns {
  204. if fullTransformPath.range(of: dirPattern) != nil {
  205. continue whileLoop
  206. }
  207. }
  208. checkFile(
  209. fullTransformPath,
  210. logger: logger,
  211. inRepo: repoURL,
  212. isSwiftFile: file.hasSuffix(".swift")
  213. )
  214. }
  215. }
  216. }
  217. return logger.foundError ? 1 : 0
  218. }
  219. exit(main())