check_imports.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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/master/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/", "FirebaseStorage/Tests/Integration",
  24. "FirebaseDynamicLinks/Tests/Integration",
  25. "FirebaseInAppMessaging/Tests/Integration/",
  26. "Example/InstanceID/App", "SymbolCollisionTest/", "/gen/",
  27. "CocoapodsIntegrationTest/"] +
  28. [
  29. "CoreOnly/Sources", // Skip Firebase.h.
  30. "SwiftPMTests", // The SwiftPM imports test module imports.
  31. ] +
  32. // The following are temporary skips pending working through a first pass of the repo:
  33. [
  34. "FirebaseAppDistribution",
  35. "FirebaseCore/Sources/Private", // TODO: work through adding this back.
  36. "Firebase/CoreDiagnostics",
  37. "FirebaseDatabase/Sources/third_party/Wrap-leveldb", // Pending SwiftPM for leveldb.
  38. "Example",
  39. "FirebaseInstallations/Source/Tests/Unit/",
  40. "Firestore",
  41. "GoogleUtilitiesComponents",
  42. ]
  43. // Skip existence test for patterns that start with the following:
  44. let skipImportPatterns = [
  45. "FBLPromise",
  46. "OCMock",
  47. "OCMStubRecorder",
  48. ]
  49. private class ErrorLogger {
  50. var foundError = false
  51. func log(_ message: String) {
  52. print(message)
  53. foundError = true
  54. }
  55. func importLog(_ message: String, _ file: String, _ line: Int) {
  56. log("Import Error: \(file):\(line) \(message)")
  57. }
  58. }
  59. private func checkFile(_ file: String, logger: ErrorLogger, inRepo repoURL: URL) {
  60. var fileContents = ""
  61. do {
  62. fileContents = try String(contentsOfFile: file, encoding: .utf8)
  63. } catch {
  64. logger.log("Could not read \(file). \(error)")
  65. // Not a source file, give up and return.
  66. return
  67. }
  68. let isPublic = file.range(of: "/Public/") != nil &&
  69. // TODO: Skip legacy GDTCCTLibrary file that isn't Public and should be moved.
  70. file.range(of: "GDTCCTLibrary/Public/GDTCOREvent+GDTCCTSupport.h") == nil
  71. let isPrivate = file.range(of: "/Sources/Private/") != nil ||
  72. // Delete when FirebaseInstallations fixes directory structure.
  73. file.range(of: "Source/Library/Private/FirebaseInstallationsInternal.h") != nil ||
  74. file.range(of: "GDTCORLibrary/Internal/GoogleDataTransportInternal.h") != nil
  75. var inSwiftPackage = false
  76. var inSwiftPackageElse = false
  77. let lines = fileContents.components(separatedBy: .newlines)
  78. var lineNum = 0
  79. nextLine: for rawLine in lines {
  80. let line = rawLine.trimmingCharacters(in: .whitespaces)
  81. lineNum += 1
  82. if line.starts(with: "#if SWIFT_PACKAGE") {
  83. inSwiftPackage = true
  84. } else if inSwiftPackage, line.starts(with: "#else") {
  85. inSwiftPackage = false
  86. inSwiftPackageElse = true
  87. } else if inSwiftPackageElse, line.starts(with: "#endif") {
  88. inSwiftPackageElse = false
  89. } else if inSwiftPackage {
  90. continue
  91. } else if line.starts(with: "@import") {
  92. // "@import" is only allowed for Swift Package Manager.
  93. logger.importLog("@import should not be used in CocoaPods library code", file, lineNum)
  94. }
  95. // "The #else of a SWIFT_PACKAGE check should only do CocoaPods module-style imports."
  96. if line.starts(with: "#import") || line.starts(with: "#include") {
  97. let importFile = line.components(separatedBy: " ")[1]
  98. if inSwiftPackageElse {
  99. if importFile.first != "<" {
  100. logger
  101. .importLog("Import in SWIFT_PACKAGE #else should start with \"<\".", file, lineNum)
  102. }
  103. continue
  104. }
  105. let importFileRaw = importFile.replacingOccurrences(of: "\"", with: "")
  106. .replacingOccurrences(of: "<", with: "")
  107. .replacingOccurrences(of: ">", with: "")
  108. if importFile.first == "\"" {
  109. // Public Headers should only use simple file names without paths.
  110. if isPublic {
  111. if importFile.contains("/") {
  112. logger.importLog("Public header import should not include \"/\"", file, lineNum)
  113. }
  114. } else if !FileManager.default.fileExists(atPath: repoURL.path + "/" + importFileRaw) {
  115. // Non-public header imports should be repo-relative paths. Unqualified imports are
  116. // allowed in private headers.
  117. if !isPrivate || importFile.contains("/") {
  118. for skip in skipImportPatterns {
  119. if importFileRaw.starts(with: skip) {
  120. continue nextLine
  121. }
  122. }
  123. logger.importLog("Import \(importFileRaw) does not exist.", file, lineNum)
  124. }
  125. }
  126. } else if importFile.first == "<", !isPrivate {
  127. // Verify that double quotes are always used for intra-module imports.
  128. if importFileRaw.starts(with: "Firebase") ||
  129. importFileRaw.starts(with: "GoogleDataTransport") {
  130. logger
  131. .importLog("Imports internal to the repo should use double quotes not \"<\"", file,
  132. lineNum)
  133. }
  134. }
  135. }
  136. }
  137. }
  138. private func main() -> Int32 {
  139. let logger = ErrorLogger()
  140. // Search the path upwards to find the root of the firebase-ios-sdk repo.
  141. var url = URL(fileURLWithPath: FileManager().currentDirectoryPath)
  142. while url.path != "/" {
  143. let script = url.appendingPathComponent("scripts/check_imports.swift")
  144. if FileManager.default.fileExists(atPath: script.path) {
  145. break
  146. }
  147. url = url.deletingLastPathComponent()
  148. }
  149. let repoURL = url
  150. guard let contents = try? FileManager.default.contentsOfDirectory(at: repoURL,
  151. includingPropertiesForKeys: nil,
  152. options: [.skipsHiddenFiles])
  153. else {
  154. logger.log("Failed to get repo contents \(repoURL)")
  155. return 1
  156. }
  157. for rootURL in contents {
  158. if !rootURL.hasDirectoryPath {
  159. continue
  160. }
  161. let enumerator = FileManager.default.enumerator(atPath: rootURL.path)
  162. whileLoop: while let file = enumerator?.nextObject() as? String {
  163. if let fType = enumerator?.fileAttributes?[FileAttributeKey.type] as? FileAttributeType,
  164. fType == .typeRegular {
  165. if file.starts(with: ".") {
  166. continue
  167. }
  168. if !(file.hasSuffix(".h") ||
  169. file.hasSuffix(".m") ||
  170. file.hasSuffix(".mm") ||
  171. file.hasSuffix(".c")) {
  172. continue
  173. }
  174. let fullTransformPath = rootURL.path + "/" + file
  175. for dirPattern in skipDirPatterns {
  176. if fullTransformPath.range(of: dirPattern) != nil {
  177. continue whileLoop
  178. }
  179. }
  180. checkFile(fullTransformPath, logger: logger, inRepo: repoURL)
  181. }
  182. }
  183. }
  184. return logger.foundError ? 1 : 0
  185. }
  186. exit(main())