Dangerfile.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import Foundation
  2. import Danger
  3. fileprivate extension Danger.File {
  4. var isInSources: Bool { hasPrefix("Sources/") }
  5. var isInTests: Bool { hasPrefix("Tests/") }
  6. var isInDemos: Bool { hasPrefix("Demos/") }
  7. var isInBenchmarking: Bool { hasPrefix("Benchmarking/") }
  8. var isInVendor: Bool { contains("/Vendor/") }
  9. var isInFMDB: Bool { contains("/FMDB/") }
  10. var isSourceFile: Bool {
  11. hasSuffix(".swift") || hasSuffix(".h") || hasSuffix(".m")
  12. }
  13. private static let spmOnlyTargetNames: Set<String> = [
  14. "CocoaLumberjackSwiftLogBackend",
  15. ]
  16. var isSPMOnlySourceFile: Bool {
  17. guard isSourceFile else { return false }
  18. if isInSources {
  19. return Self.spmOnlyTargetNames.contains(where: { contains("/\($0)/") })
  20. } else if isInTests {
  21. return Self.spmOnlyTargetNames.contains(where: { contains("/\($0)Tests/") })
  22. }
  23. return false
  24. }
  25. var isSwiftPackageDefintion: Bool {
  26. hasPrefix("Package") && hasSuffix(".swift")
  27. }
  28. var isDangerfile: Bool {
  29. self == "Dangerfile.swift"
  30. }
  31. }
  32. let danger = Danger()
  33. let git = danger.git
  34. // Sometimes it's a README fix, or something like that - which isn't relevant for
  35. // including in a project's CHANGELOG for example
  36. let isDeclaredTrivial = danger.github?.pullRequest.title.contains("#trivial") ?? false
  37. let hasSourceChanges = (git.modifiedFiles + git.createdFiles).contains { $0.isInSources }
  38. // Make it more obvious that a PR is a work in progress and shouldn't be merged yet
  39. if danger.github?.pullRequest.title.contains("WIP") == true {
  40. warn("PR is marked as Work in Progress")
  41. }
  42. // Warn when there is a big PR
  43. if let additions = danger.github?.pullRequest.additions, let deletions = danger.github?.pullRequest.deletions,
  44. case let sum = additions + deletions, sum > 1000 {
  45. warn("Pull request is relatively big (\(sum) lines changed). If this PR contains multiple changes, consider splitting it into separate PRs for easier reviews.")
  46. }
  47. // Changelog entries are required for changes to library files.
  48. if hasSourceChanges && !isDeclaredTrivial && !git.modifiedFiles.contains("CHANGELOG.md") {
  49. warn("Any changes to library code should be reflected in the CHANGELOG. Please consider adding a note there about your change.")
  50. }
  51. // Warn when library files has been updated but not tests.
  52. if hasSourceChanges && !git.modifiedFiles.contains(where: { $0.isInTests }) {
  53. warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.")
  54. }
  55. // Run SwiftLint
  56. SwiftLint.lint(.modifiedAndCreatedFiles(directory: "Sources"))
  57. // Added (or removed) library files need to be added (or removed) from the
  58. // Carthage Xcode project to avoid breaking things for our Carthage users.
  59. let xcodeProjectFile: Danger.File = "Lumberjack.xcodeproj/project.pbxproj"
  60. let xcodeProjectWasModified = git.modifiedFiles.contains(xcodeProjectFile)
  61. if (git.createdFiles + git.deletedFiles).contains(where: { $0.isInSources && $0.isSourceFile && !$0.isSPMOnlySourceFile })
  62. && !xcodeProjectWasModified {
  63. fail("Added or removed library files require the Carthage Xcode project to be updated.")
  64. }
  65. // Check if Carthage modified and CocoaPods didn't or vice-versa
  66. let podspecWasModified = git.modifiedFiles.contains("CocoaLumberjack.podspec")
  67. if xcodeProjectWasModified && !podspecWasModified {
  68. warn("The Carthage project was modified but CocoaPods podspec wasn't. Did you forget to update the podspec?")
  69. }
  70. if !xcodeProjectWasModified && podspecWasModified {
  71. warn("The CocoaPods podspec was modified but the Carthage project wasn't. Did you forget to update the xcodeproj?")
  72. }
  73. // Check xcodeproj settings are not changed
  74. // Check to see if any of our project files contains a line with "SOURCE_ROOT" which indicates that the file isn't in sync with Finder.
  75. if xcodeProjectWasModified {
  76. let acceptedSettings: Set<String> = [
  77. "APPLICATION_EXTENSION_API_ONLY",
  78. "ASSETCATALOG_COMPILER_APPICON_NAME",
  79. "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME",
  80. "ATTRIBUTES",
  81. "CODE_SIGN_IDENTITY",
  82. "COMBINE_HIDPI_IMAGES",
  83. "FRAMEWORK_VERSION",
  84. "GCC_PRECOMPILE_PREFIX_HEADER",
  85. "GCC_PREFIX_HEADER",
  86. "IBSC_MODULE",
  87. "INFOPLIST_FILE",
  88. "MODULEMAP_FILE",
  89. "PRIVATE_HEADERS_FOLDER_PATH",
  90. "PRODUCT_BUNDLE_IDENTIFIER",
  91. "PRODUCT_NAME",
  92. "PUBLIC_HEADERS_FOLDER_PATH",
  93. "SDKROOT",
  94. "SUPPORTED_PLATFORMS",
  95. "TARGETED_DEVICE_FAMILY",
  96. "WRAPPER_EXTENSION",
  97. ]
  98. [xcodeProjectFile]
  99. .lazy
  100. .filter { FileManager.default.fileExists(atPath: $0) }
  101. .forEach { projectFile in
  102. danger.utils.readFile(projectFile).split(separator: "\n").enumerated().forEach { (offset, line) in
  103. if line.contains("sourceTree = SOURCE_ROOT;") &&
  104. line.contains("PBXFileReference") &&
  105. !line.contains("path = Sources/CocoaLumberjackSwiftSupport/include/") {
  106. warn(message: "Files should be in sync with project structure", file: projectFile, line: offset + 1)
  107. }
  108. if let range = line.range(of: "[A-Z_]+ = .*;", options: .regularExpression) {
  109. let setting = String(line[range].prefix(while: { $0 != " " }))
  110. if !acceptedSettings.contains(setting) {
  111. warn(message: "Xcode settings need to remain in Configs/*.xcconfig. Please move " + setting + " to the xcconfig file", file: projectFile, line: offset + 1)
  112. }
  113. }
  114. }
  115. }
  116. }
  117. // Check Copyright
  118. let copyrightLines = (
  119. source: [
  120. "// Software License Agreement (BSD License)",
  121. "//",
  122. "// Copyright (c) 2010-2021, Deusty, LLC",
  123. "// All rights reserved.",
  124. "//",
  125. "// Redistribution and use of this software in source and binary forms,",
  126. "// with or without modification, are permitted provided that the following conditions are met:",
  127. "//",
  128. "// * Redistributions of source code must retain the above copyright notice,",
  129. "// this list of conditions and the following disclaimer.",
  130. "//",
  131. "// * Neither the name of Deusty nor the names of its contributors may be used",
  132. "// to endorse or promote products derived from this software without specific",
  133. "// prior written permission of Deusty, LLC.",
  134. ],
  135. demos: [
  136. "//",
  137. "// ",
  138. "// ",
  139. "//",
  140. "// CocoaLumberjack Demos",
  141. "//",
  142. ],
  143. benchmarking: [
  144. "//",
  145. "// ",
  146. "// ",
  147. "//",
  148. "// CocoaLumberjack Benchmarking",
  149. "//",
  150. ]
  151. )
  152. // let sourcefilesToCheck = Dir.glob("*/*/*") // uncomment when we want to test all the files (locally)
  153. let sourcefilesToCheck = Set(git.modifiedFiles + git.createdFiles)
  154. let filesWithInvalidCopyright = sourcefilesToCheck.lazy
  155. .filter { $0.isSourceFile }
  156. .filter { !$0.isSwiftPackageDefintion }
  157. .filter { !$0.isDangerfile }
  158. .filter { !$0.isInVendor && !$0.isInFMDB }
  159. .filter { FileManager.default.fileExists(atPath: $0) }
  160. .filter {
  161. // Use correct copyright lines depending on source file location
  162. let (expectedLines, shouldMatchExactly): (Array<String>, Bool)
  163. if $0.isInDemos {
  164. expectedLines = copyrightLines.demos
  165. shouldMatchExactly = false
  166. } else if $0.isInBenchmarking {
  167. expectedLines = copyrightLines.benchmarking
  168. shouldMatchExactly = false
  169. } else {
  170. expectedLines = copyrightLines.source
  171. shouldMatchExactly = true
  172. }
  173. let actualLines = danger.utils.readFile($0).split(separator: "\n").lazy.map(String.init)
  174. if shouldMatchExactly {
  175. return !actualLines.starts(with: expectedLines)
  176. } else {
  177. return !zip(actualLines, expectedLines).allSatisfy { $0.starts(with: $1) }
  178. }
  179. }
  180. if !filesWithInvalidCopyright.isEmpty {
  181. filesWithInvalidCopyright.forEach {
  182. markdown(message: "Invalid copyright!", file: $0, line: 1)
  183. }
  184. warn("""
  185. Copyright is not valid. See our default copyright in all of our files (Sources, Demos and Benchmarking use different formats).
  186. Invalid files:
  187. \(filesWithInvalidCopyright.map { "- \($0)" }.joined(separator: "\n"))
  188. """)
  189. }