check_imports.swift 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. "FirebaseInAppMessaging/Tests/Integration/",
  25. "Example/InstanceID/App", "SymbolCollisionTest/", "/gen/",
  26. "CocoapodsIntegrationTest/"] +
  27. [
  28. "CoreOnly/Sources", // Skip Firebase.h
  29. ] +
  30. // The following are temporary skips pending working through a first pass of the repo:
  31. [
  32. "FirebaseABTesting",
  33. "FirebaseAppDistribution",
  34. "FirebaseCore/Sources/Private", // Fixes require breaking private API changes. For Firebase 7.
  35. "FirebaseDynamicLinks",
  36. "Firebase/CoreDiagnostics",
  37. "FirebaseDatabase/Sources/third_party/Wrap-leveldb", // Pending SwiftPM for leveldb.
  38. "Example",
  39. "FirebaseInAppMessaging",
  40. "FirebaseInstallations/Source/Tests/Unit/",
  41. "Firebase/InstanceID",
  42. "FirebaseMessaging",
  43. "FirebaseRemoteConfig",
  44. "Crashlytics",
  45. "Firestore",
  46. "GoogleDataTransport",
  47. "GoogleUtilitiesComponents",
  48. ]
  49. // Skip existence test for patterns that start with the following:
  50. let skipImportPatterns = [
  51. "FBLPromise",
  52. ]
  53. var foundError = false
  54. func genError(_ message: String) {
  55. print(message)
  56. foundError = true
  57. }
  58. func checkFile(_ file: String, isPublic: Bool) {
  59. var fileContents = ""
  60. do {
  61. fileContents = try String(contentsOfFile: file, encoding: .utf8)
  62. } catch {
  63. genError("Could not read \(file). \(error)")
  64. // Not a source file, give up and return.
  65. return
  66. }
  67. var inSwiftPackage = false
  68. var inSwiftPackageElse = false
  69. let lines = fileContents.components(separatedBy: .newlines)
  70. var lineNum = 0
  71. nextLine: for rawLine in lines {
  72. let line = rawLine.trimmingCharacters(in: .whitespaces)
  73. lineNum += 1
  74. if line.starts(with: "#if SWIFT_PACKAGE") {
  75. inSwiftPackage = true
  76. } else if inSwiftPackage, line.starts(with: "#else") {
  77. inSwiftPackage = false
  78. inSwiftPackageElse = true
  79. } else if inSwiftPackageElse, line.starts(with: "#endif") {
  80. inSwiftPackageElse = false
  81. } else if inSwiftPackage {
  82. continue
  83. } else if line.starts(with: "@import") {
  84. // "@import" is only allowed for Swift Package Manager.
  85. genError("@import should not be used in CocoaPods library code: \(file):\(lineNum)")
  86. }
  87. // "The #else of a SWIFT_PACKAGE check should only do CocoaPods module-style imports."
  88. if line.starts(with: "#import") || line.starts(with: "#include") {
  89. let importFile = line.components(separatedBy: " ")[1]
  90. if inSwiftPackageElse {
  91. if importFile.first != "<" {
  92. genError("Import error: \(file):\(lineNum) Import in SWIFT_PACKAGE #else should start with \"<\".")
  93. }
  94. continue
  95. }
  96. let importFileRaw = importFile.replacingOccurrences(of: "\"", with: "")
  97. .replacingOccurrences(of: "<", with: "")
  98. .replacingOccurrences(of: ">", with: "")
  99. if importFile.first == "\"" {
  100. // Public Headers should only use simple file names without paths.
  101. if isPublic {
  102. if importFile.contains("/") {
  103. genError("Import error: \(file):\(lineNum) Public header import should not include \"/\"")
  104. }
  105. } else if !FileManager.default.fileExists(atPath: repoURL.path + "/" + importFileRaw) {
  106. // All non-public header imports should be repo-relative paths.
  107. for skip in skipImportPatterns {
  108. if importFileRaw.starts(with: skip) {
  109. continue nextLine
  110. }
  111. }
  112. genError("Import error: \(file):\(lineNum) Import \(importFileRaw) does not exist.")
  113. }
  114. } else if importFile.first == "<" {
  115. // Verify that double quotes are always used for intra-module imports.
  116. if importFileRaw.starts(with: "Firebase") ||
  117. importFileRaw.starts(with: "GoogleUtilities") ||
  118. importFileRaw.starts(with: "GoogleDataTransport") {
  119. genError("Import error: \(file):\(lineNum) Imports internal to the repo should use double quotes not \"<\"")
  120. }
  121. }
  122. }
  123. }
  124. }
  125. // Search the path upwards to find the root of the firebase-ios-sdk repo.
  126. var url = URL(fileURLWithPath: FileManager().currentDirectoryPath)
  127. while url.path != "/", url.lastPathComponent != "firebase-ios-sdk" {
  128. url = url.deletingLastPathComponent()
  129. }
  130. let repoURL = url
  131. let contents =
  132. try FileManager.default.contentsOfDirectory(at: repoURL,
  133. includingPropertiesForKeys: nil,
  134. options: [.skipsHiddenFiles])
  135. for rootURL in contents {
  136. if !rootURL.hasDirectoryPath {
  137. continue
  138. }
  139. let enumerator = FileManager.default.enumerator(atPath: rootURL.path)
  140. whileLoop: while let file = enumerator?.nextObject() as? String {
  141. if let fType = enumerator?.fileAttributes?[FileAttributeKey.type] as? FileAttributeType,
  142. fType == .typeRegular {
  143. if file.starts(with: ".") {
  144. continue
  145. }
  146. if !(file.hasSuffix(".h") ||
  147. file.hasSuffix(".m") ||
  148. file.hasSuffix(".mm") ||
  149. file.hasSuffix(".c")) {
  150. continue
  151. }
  152. let fullTransformPath = rootURL.path + "/" + file
  153. for dirPattern in skipDirPatterns {
  154. if fullTransformPath.range(of: dirPattern) != nil {
  155. continue whileLoop
  156. }
  157. }
  158. checkFile(fullTransformPath, isPublic: file.range(of: "/Public/") != nil)
  159. }
  160. }
  161. }
  162. exit(foundError ? 1 : 0)