import Foundation import Danger fileprivate extension Danger.File { var isInSources: Bool { hasPrefix("Sources/") } var isInTests: Bool { hasPrefix("Tests/") } var isInDemos: Bool { hasPrefix("Demos/") } var isInBenchmarking: Bool { hasPrefix("Benchmarking/") } var isInVendor: Bool { contains("/Vendor/") } var isInFMDB: Bool { contains("/FMDB/") } var isSourceFile: Bool { fileType == .swift || fileType == .m || fileType == .mm || fileType == .h } private static let spmOnlyTargetNames: Set = [ "CocoaLumberjackSwiftLogBackend", ] var isSPMOnlySourceFile: Bool { guard isSourceFile else { return false } if isInSources { return Self.spmOnlyTargetNames.contains(where: { contains("/\($0)/") }) } else if isInTests { return Self.spmOnlyTargetNames.contains(where: { contains("/\($0)Tests/") }) } return false } var isSwiftPackageDefintion: Bool { hasPrefix("Package") && fileType == .swift } var isDangerfile: Bool { self == "Dangerfile.swift" } } let danger = Danger() let git = danger.git // Sometimes it's a README fix, or something like that let isDeclaredTrivial = danger.github?.pullRequest.title.contains("#trivial") ?? false let hasSourceChanges = (git.modifiedFiles + git.createdFiles).contains(where: \.isInSources) // Make it more obvious that a PR is a work in progress and shouldn't be merged yet if danger.github?.pullRequest.title.contains("WIP") == true && danger.github?.pullRequest.draft != true { warn("PR is marked as Work in Progress. Please consider marking the PR as Draft in GitHub to prevent merges.") } // Warn when there is a big PR if let additions = danger.github?.pullRequest.additions, let deletions = danger.github?.pullRequest.deletions, case let sum = additions + deletions, sum > 1000 { warn("Pull request is relatively big (\(sum) lines changed). If this PR contains multiple changes, consider splitting it into separate PRs for easier reviews.") } // Warn when library files has been updated but not tests. if hasSourceChanges && !git.modifiedFiles.contains(where: \.isInTests) { warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.") } // Run SwiftLint SwiftLint.lint(.modifiedAndCreatedFiles(directory: "Sources"), inline: true) // Added (or removed) library files need to be added (or removed) from the // Carthage Xcode project to avoid breaking things for our Carthage users. let xcodeProjectFile: Danger.File = "Lumberjack.xcodeproj/project.pbxproj" let xcodeProjectWasModified = git.modifiedFiles.contains(xcodeProjectFile) if (git.createdFiles + git.deletedFiles).contains(where: { $0.isInSources && $0.isSourceFile && !$0.isSPMOnlySourceFile }) && !xcodeProjectWasModified { fail("Added or removed library files require the Carthage Xcode project to be updated.") } // Check xcodeproj settings are not changed // 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. if xcodeProjectWasModified { let acceptedSettings: Set = [ "APPLICATION_EXTENSION_API_ONLY", "ASSETCATALOG_COMPILER_APPICON_NAME", "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME", "ATTRIBUTES", "CODE_SIGN_IDENTITY", "COMBINE_HIDPI_IMAGES", "FRAMEWORK_VERSION", "GCC_PRECOMPILE_PREFIX_HEADER", "GCC_PREFIX_HEADER", "IBSC_MODULE", "INFOPLIST_FILE", "MODULEMAP_FILE", "PRIVATE_HEADERS_FOLDER_PATH", "PRODUCT_BUNDLE_IDENTIFIER", "PRODUCT_NAME", "PUBLIC_HEADERS_FOLDER_PATH", "SDKROOT", "SUPPORTED_PLATFORMS", "TARGETED_DEVICE_FAMILY", "WRAPPER_EXTENSION", ] [xcodeProjectFile] .lazy .filter { FileManager.default.fileExists(atPath: $0) } .forEach { projectFile in danger.utils.readFile(projectFile).split(separator: "\n").enumerated().forEach { (offset, line) in if line.contains("sourceTree = SOURCE_ROOT;") && line.contains("PBXFileReference") && !line.contains("path = Sources/CocoaLumberjackSwiftSupport/include/") { warn(message: "Files should be in sync with project structure", file: projectFile, line: offset + 1) } if let range = line.range(of: "[A-Z_]+ = .*;", options: .regularExpression) { let setting = String(line[range].prefix(while: { $0 != " " })) if !acceptedSettings.contains(setting) { warn(message: "Xcode settings need to remain in Configs/*.xcconfig. Please move " + setting + " to the xcconfig file", file: projectFile, line: offset + 1) } } } } } // Check Copyright let copyrightLines = ( source: [ "// Software License Agreement (BSD License)", "//", "// Copyright (c) 2010-2025, Deusty, LLC", "// All rights reserved.", "//", "// Redistribution and use of this software in source and binary forms,", "// with or without modification, are permitted provided that the following conditions are met:", "//", "// * Redistributions of source code must retain the above copyright notice,", "// this list of conditions and the following disclaimer.", "//", "// * Neither the name of Deusty nor the names of its contributors may be used", "// to endorse or promote products derived from this software without specific", "// prior written permission of Deusty, LLC.", ], demos: [ "//", "// ", "// ", "//", "// CocoaLumberjack Demos", "//", ], benchmarking: [ "//", "// ", "// ", "//", "// CocoaLumberjack Benchmarking", "//", ] ) // let sourcefilesToCheck = Dir.glob("*/*/*") // uncomment when we want to test all the files (locally) let sourcefilesToCheck = Set(git.modifiedFiles + git.createdFiles) let filesWithInvalidCopyright = sourcefilesToCheck.lazy .filter { $0.isSourceFile } .filter { !$0.isSwiftPackageDefintion } .filter { !$0.isDangerfile } .filter { !$0.isInVendor && !$0.isInFMDB } .filter { FileManager.default.fileExists(atPath: $0) } .filter { // Use correct copyright lines depending on source file location let (expectedLines, shouldMatchExactly): (Array, Bool) if $0.isInDemos { expectedLines = copyrightLines.demos shouldMatchExactly = false } else if $0.isInBenchmarking { expectedLines = copyrightLines.benchmarking shouldMatchExactly = false } else { expectedLines = copyrightLines.source shouldMatchExactly = true } let actualLines = danger.utils.readFile($0).split(separator: "\n").lazy.map(String.init) if shouldMatchExactly { return !actualLines.starts(with: expectedLines) } else { return !zip(actualLines, expectedLines).allSatisfy { $0.starts(with: $1) } } } if !filesWithInvalidCopyright.isEmpty { filesWithInvalidCopyright.sorted().forEach { warn(message: "Invalid copyright!", file: $0, line: 1) } warn(""" Copyright is not valid. See our default copyright in all of our files (Sources, Demos and Benchmarking use different formats).
Invalid files:
\(filesWithInvalidCopyright.map { "- \($0)" }.joined(separator: "
\n")) """) }