Dangerfile.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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. fileType == .swift || fileType == .m || fileType == .mm || fileType == .h
  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") && fileType == .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,
  44. let deletions = danger.github?.pullRequest.deletions,
  45. case let sum = additions + deletions, sum > 1000 {
  46. warn("Pull request is relatively big (\(sum) lines changed). If this PR contains multiple changes, consider splitting it into separate PRs for easier reviews.")
  47. }
  48. // Changelog entries are required for changes to library files.
  49. if hasSourceChanges && !isDeclaredTrivial && !git.modifiedFiles.contains("CHANGELOG.md") {
  50. warn("Any changes to library code should be reflected in the CHANGELOG. Please consider adding a note there about your change.")
  51. }
  52. // Warn when library files has been updated but not tests.
  53. if hasSourceChanges && !git.modifiedFiles.contains(where: { $0.isInTests }) {
  54. warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.")
  55. }
  56. // Run SwiftLint
  57. SwiftLint.lint(.modifiedAndCreatedFiles(directory: "Sources"), inline: true)
  58. // Added (or removed) library files need to be added (or removed) from the
  59. // Carthage Xcode project to avoid breaking things for our Carthage users.
  60. let xcodeProjectFile: Danger.File = "Lumberjack.xcodeproj/project.pbxproj"
  61. let xcodeProjectWasModified = git.modifiedFiles.contains(xcodeProjectFile)
  62. if (git.createdFiles + git.deletedFiles).contains(where: { $0.isInSources && $0.isSourceFile && !$0.isSPMOnlySourceFile })
  63. && !xcodeProjectWasModified {
  64. fail("Added or removed library files require the Carthage Xcode project to be updated.")
  65. }
  66. // Check xcodeproj settings are not changed
  67. // 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.
  68. if xcodeProjectWasModified {
  69. let acceptedSettings: Set<String> = [
  70. "APPLICATION_EXTENSION_API_ONLY",
  71. "ASSETCATALOG_COMPILER_APPICON_NAME",
  72. "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME",
  73. "ATTRIBUTES",
  74. "CODE_SIGN_IDENTITY",
  75. "COMBINE_HIDPI_IMAGES",
  76. "FRAMEWORK_VERSION",
  77. "GCC_PRECOMPILE_PREFIX_HEADER",
  78. "GCC_PREFIX_HEADER",
  79. "IBSC_MODULE",
  80. "INFOPLIST_FILE",
  81. "MODULEMAP_FILE",
  82. "PRIVATE_HEADERS_FOLDER_PATH",
  83. "PRODUCT_BUNDLE_IDENTIFIER",
  84. "PRODUCT_NAME",
  85. "PUBLIC_HEADERS_FOLDER_PATH",
  86. "SDKROOT",
  87. "SUPPORTED_PLATFORMS",
  88. "TARGETED_DEVICE_FAMILY",
  89. "WRAPPER_EXTENSION",
  90. ]
  91. [xcodeProjectFile]
  92. .lazy
  93. .filter { FileManager.default.fileExists(atPath: $0) }
  94. .forEach { projectFile in
  95. danger.utils.readFile(projectFile).split(separator: "\n").enumerated().forEach { (offset, line) in
  96. if line.contains("sourceTree = SOURCE_ROOT;") &&
  97. line.contains("PBXFileReference") &&
  98. !line.contains("path = Sources/CocoaLumberjackSwiftSupport/include/") {
  99. warn(message: "Files should be in sync with project structure", file: projectFile, line: offset + 1)
  100. }
  101. if let range = line.range(of: "[A-Z_]+ = .*;", options: .regularExpression) {
  102. let setting = String(line[range].prefix(while: { $0 != " " }))
  103. if !acceptedSettings.contains(setting) {
  104. warn(message: "Xcode settings need to remain in Configs/*.xcconfig. Please move " + setting + " to the xcconfig file", file: projectFile, line: offset + 1)
  105. }
  106. }
  107. }
  108. }
  109. }
  110. // Check Copyright
  111. let copyrightLines = (
  112. source: [
  113. "// Software License Agreement (BSD License)",
  114. "//",
  115. "// Copyright (c) 2010-2023, Deusty, LLC",
  116. "// All rights reserved.",
  117. "//",
  118. "// Redistribution and use of this software in source and binary forms,",
  119. "// with or without modification, are permitted provided that the following conditions are met:",
  120. "//",
  121. "// * Redistributions of source code must retain the above copyright notice,",
  122. "// this list of conditions and the following disclaimer.",
  123. "//",
  124. "// * Neither the name of Deusty nor the names of its contributors may be used",
  125. "// to endorse or promote products derived from this software without specific",
  126. "// prior written permission of Deusty, LLC.",
  127. ],
  128. demos: [
  129. "//",
  130. "// ",
  131. "// ",
  132. "//",
  133. "// CocoaLumberjack Demos",
  134. "//",
  135. ],
  136. benchmarking: [
  137. "//",
  138. "// ",
  139. "// ",
  140. "//",
  141. "// CocoaLumberjack Benchmarking",
  142. "//",
  143. ]
  144. )
  145. // let sourcefilesToCheck = Dir.glob("*/*/*") // uncomment when we want to test all the files (locally)
  146. let sourcefilesToCheck = Set(git.modifiedFiles + git.createdFiles)
  147. let filesWithInvalidCopyright = sourcefilesToCheck.lazy
  148. .filter { $0.isSourceFile }
  149. .filter { !$0.isSwiftPackageDefintion }
  150. .filter { !$0.isDangerfile }
  151. .filter { !$0.isInVendor && !$0.isInFMDB }
  152. .filter { FileManager.default.fileExists(atPath: $0) }
  153. .filter {
  154. // Use correct copyright lines depending on source file location
  155. let (expectedLines, shouldMatchExactly): (Array<String>, Bool)
  156. if $0.isInDemos {
  157. expectedLines = copyrightLines.demos
  158. shouldMatchExactly = false
  159. } else if $0.isInBenchmarking {
  160. expectedLines = copyrightLines.benchmarking
  161. shouldMatchExactly = false
  162. } else {
  163. expectedLines = copyrightLines.source
  164. shouldMatchExactly = true
  165. }
  166. let actualLines = danger.utils.readFile($0).split(separator: "\n").lazy.map(String.init)
  167. if shouldMatchExactly {
  168. return !actualLines.starts(with: expectedLines)
  169. } else {
  170. return !zip(actualLines, expectedLines).allSatisfy { $0.starts(with: $1) }
  171. }
  172. }
  173. if !filesWithInvalidCopyright.isEmpty {
  174. filesWithInvalidCopyright.sorted().forEach {
  175. warn(message: "Invalid copyright!", file: $0, line: 1)
  176. }
  177. warn("""
  178. Copyright is not valid. See our default copyright in all of our files (Sources, Demos and Benchmarking use different formats).<br/>
  179. Invalid files:<br/>
  180. \(filesWithInvalidCopyright.map { "- \($0)" }.joined(separator: "<br/>\n"))
  181. """)
  182. }