XCode project for app, moved app project to folder

This commit is contained in:
Joe Mattiello
2023-03-01 22:07:19 -05:00
parent 365cadbb31
commit 4c9c5b1a56
371 changed files with 625 additions and 39 deletions

View File

@@ -0,0 +1,61 @@
import ArgumentParser
import SwiftLintFramework
extension SwiftLint {
struct Build: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Print lint warnings and errors")
@OptionGroup
var common: LintOrAnalyzeArguments
@Option(help: pathOptionDescription(for: .build))
var path: String?
@Flag(help: quietOptionDescription(for: .build))
var quiet = false
@Option(help: "The directory of the cache used when linting.")
var cachePath: String?
@Flag(help: "Ignore cache when linting.")
var noCache = false
@Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.")
var enableAllRules = false
@Argument(help: pathsArgumentDescription(for: .build))
var paths = [String]()
func run() async throws {
let allPaths: [String]
if let path {
queuedPrintError("""
warning: The --path option is deprecated. Pass the path(s) to lint last to the swiftlint command.
""")
allPaths = [path] + paths
} else if !paths.isEmpty {
allPaths = paths
} else {
allPaths = [""] // Lint files in current working directory if no paths were specified.
}
let options = LintOrAnalyzeOptions(
mode: .build,
paths: allPaths,
configurationFiles: common.config,
strict: common.leniency == .strict,
lenient: common.leniency == .lenient,
forceExclude: common.forceExclude,
useExcludingByPrefix: common.useAlternativeExcluding,
useScriptInputFiles: common.useScriptInputFiles,
benchmark: common.benchmark,
reporter: common.reporter,
quiet: quiet,
output: common.output,
progress: common.progress,
cachePath: cachePath,
ignoreCache: noCache,
enableAllRules: enableAllRules,
autocorrect: common.fix,
format: common.format,
compilerLogPath: nil,
compileCommands: nil,
inProcessSourcekit: common.inProcessSourcekit
)
try await LintOrAnalyzeCommand.run(options)
}
}
}

View File

@@ -0,0 +1,22 @@
import ArgumentParser
import Foundation
@main
struct Cargo: AsyncParsableCommand {
static let configuration: CommandConfiguration = {
if let directory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] {
FileManager.default.changeCurrentDirectoryPath(directory)
}
return CommandConfiguration(
commandName: "cargo",
abstract: "A tool to build `rust` projects with `cargo`.",
version: Version.value,
subcommands: [
Build.self,
Version.self
],
defaultSubcommand: Build.self
)
}()
}

View File

@@ -0,0 +1,20 @@
extension RulesFilter.ExcludingOptions {
static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self {
var excludingOptions: Self = []
switch rulesFilterOptions.ruleEnablement {
case .enabled:
excludingOptions.insert(.disabled)
case .disabled:
excludingOptions.insert(.enabled)
case .none:
break
}
if rulesFilterOptions.correctable {
excludingOptions.insert(.uncorrectable)
}
return excludingOptions
}
}

View File

@@ -0,0 +1,20 @@
import ArgumentParser
enum RuleEnablementOptions: String, EnumerableFlag {
case enabled, disabled
static func name(for value: RuleEnablementOptions) -> NameSpecification {
return .shortAndLong
}
static func help(for value: RuleEnablementOptions) -> ArgumentHelp? {
return "Only show \(value.rawValue) rules"
}
}
struct RulesFilterOptions: ParsableArguments {
@Flag(exclusivity: .exclusive)
var ruleEnablement: RuleEnablementOptions?
@Flag(name: .shortAndLong, help: "Only display correctable rules")
var correctable = false
}

View File

@@ -0,0 +1,23 @@
import ArgumentParser
import SwiftLintFramework
extension Cargo {
struct Version: ParsableCommand {
@Flag(help: "Display full version info")
var verbose = false
static let configuration = CommandConfiguration(abstract: "Display the current version of Cargo")
static var value: String { "TODO" }
func run() throws {
if verbose, let buildID = ExecutableInfo.buildID {
print("Version:", Self.value)
print("Build ID:", buildID)
} else {
print(Self.value)
}
ExitHelper.successfullyExit()
}
}
}

View File

@@ -0,0 +1,61 @@
import ArgumentParser
import SwiftLintFramework
extension SwiftLint {
struct Analyze: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Run analysis rules")
@OptionGroup
var common: LintOrAnalyzeArguments
@Option(help: pathOptionDescription(for: .analyze))
var path: String?
@Flag(help: quietOptionDescription(for: .analyze))
var quiet = false
@Option(help: "The path of the full xcodebuild log to use when running AnalyzerRules.")
var compilerLogPath: String?
@Option(help: "The path of a compilation database to use when running AnalyzerRules.")
var compileCommands: String?
@Argument(help: pathsArgumentDescription(for: .analyze))
var paths = [String]()
func run() async throws {
let allPaths: [String]
if let path {
queuedPrintError("""
warning: The --path option is deprecated. Pass the path(s) to analyze last to the swiftlint command.
""")
allPaths = [path] + paths
} else if !paths.isEmpty {
allPaths = paths
} else {
allPaths = [""] // Analyze files in current working directory if no paths were specified.
}
let options = LintOrAnalyzeOptions(
mode: .analyze,
paths: allPaths,
useSTDIN: false,
configurationFiles: common.config,
strict: common.leniency == .strict,
lenient: common.leniency == .lenient,
forceExclude: common.forceExclude,
useExcludingByPrefix: common.useAlternativeExcluding,
useScriptInputFiles: common.useScriptInputFiles,
benchmark: common.benchmark,
reporter: common.reporter,
quiet: quiet,
output: common.output,
progress: common.progress,
cachePath: nil,
ignoreCache: true,
enableAllRules: false,
autocorrect: common.fix,
format: common.format,
compilerLogPath: compilerLogPath,
compileCommands: compileCommands,
inProcessSourcekit: common.inProcessSourcekit
)
try await LintOrAnalyzeCommand.run(options)
}
}
}

View File

@@ -0,0 +1,20 @@
extension RulesFilter.ExcludingOptions {
static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self {
var excludingOptions: Self = []
switch rulesFilterOptions.ruleEnablement {
case .enabled:
excludingOptions.insert(.disabled)
case .disabled:
excludingOptions.insert(.enabled)
case .none:
break
}
if rulesFilterOptions.correctable {
excludingOptions.insert(.uncorrectable)
}
return excludingOptions
}
}

View File

@@ -0,0 +1,20 @@
import ArgumentParser
enum RuleEnablementOptions: String, EnumerableFlag {
case enabled, disabled
static func name(for value: RuleEnablementOptions) -> NameSpecification {
return .shortAndLong
}
static func help(for value: RuleEnablementOptions) -> ArgumentHelp? {
return "Only show \(value.rawValue) rules"
}
}
struct RulesFilterOptions: ParsableArguments {
@Flag(exclusivity: .exclusive)
var ruleEnablement: RuleEnablementOptions?
@Flag(name: .shortAndLong, help: "Only display correctable rules")
var correctable = false
}

View File

@@ -0,0 +1,43 @@
import ArgumentParser
import Foundation
import SwiftLintFramework
extension SwiftLint {
struct Docs: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open SwiftLint documentation website in the default web browser"
)
@Argument(help: "The identifier of the rule to open the documentation for")
var ruleID: String?
func run() throws {
var subPage = ""
if let ruleID {
if primaryRuleList.list[ruleID] == nil {
queuedPrintError("There is no rule named '\(ruleID)'. Opening rule directory instead.")
subPage = "rule-directory.html"
} else {
subPage = ruleID + ".html"
}
}
open(URL(string: "https://realm.github.io/SwiftLint/\(subPage)")!)
ExitHelper.successfullyExit()
}
}
}
private func open(_ url: URL) {
let process = Process()
#if os(Linux)
process.executableURL = URL(fileURLWithPath: "/usr/bin/env", isDirectory: false)
let command = "xdg-open"
process.arguments = [command, url.absoluteString]
try? process.run()
#else
process.launchPath = "/usr/bin/env"
let command = "open"
process.arguments = [command, url.absoluteString]
process.launch()
#endif
}

View File

@@ -0,0 +1,28 @@
import ArgumentParser
import Foundation
import SwiftLintFramework
extension SwiftLint {
struct GenerateDocs: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Generates markdown documentation for selected group of rules"
)
@Option(help: "The directory where the documentation should be saved")
var path = "rule_docs"
@Option(help: "The path to a SwiftLint configuration file")
var config: String?
@OptionGroup
var rulesFilterOptions: RulesFilterOptions
func run() throws {
let configuration = Configuration(configurationFiles: [config].compactMap({ $0 }))
let rulesFilter = RulesFilter(enabledRules: configuration.rules)
let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions))
try RuleListDocumentation(rules)
.write(to: URL(fileURLWithPath: path, isDirectory: true))
ExitHelper.successfullyExit()
}
}
}

View File

@@ -0,0 +1,64 @@
import ArgumentParser
import SwiftLintFramework
extension SwiftLint {
struct Lint: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Print lint warnings and errors")
@OptionGroup
var common: LintOrAnalyzeArguments
@Option(help: pathOptionDescription(for: .lint))
var path: String?
@Flag(help: "Lint standard input.")
var useSTDIN = false
@Flag(help: quietOptionDescription(for: .lint))
var quiet = false
@Option(help: "The directory of the cache used when linting.")
var cachePath: String?
@Flag(help: "Ignore cache when linting.")
var noCache = false
@Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.")
var enableAllRules = false
@Argument(help: pathsArgumentDescription(for: .lint))
var paths = [String]()
func run() async throws {
let allPaths: [String]
if let path {
queuedPrintError("""
warning: The --path option is deprecated. Pass the path(s) to lint last to the swiftlint command.
""")
allPaths = [path] + paths
} else if !paths.isEmpty {
allPaths = paths
} else {
allPaths = [""] // Lint files in current working directory if no paths were specified.
}
let options = LintOrAnalyzeOptions(
mode: .lint,
paths: allPaths,
useSTDIN: useSTDIN,
configurationFiles: common.config,
strict: common.leniency == .strict,
lenient: common.leniency == .lenient,
forceExclude: common.forceExclude,
useExcludingByPrefix: common.useAlternativeExcluding,
useScriptInputFiles: common.useScriptInputFiles,
benchmark: common.benchmark,
reporter: common.reporter,
quiet: quiet,
output: common.output,
progress: common.progress,
cachePath: cachePath,
ignoreCache: noCache,
enableAllRules: enableAllRules,
autocorrect: common.fix,
format: common.format,
compilerLogPath: nil,
compileCommands: nil,
inProcessSourcekit: common.inProcessSourcekit
)
try await LintOrAnalyzeCommand.run(options)
}
}
}

View File

@@ -0,0 +1,123 @@
import ArgumentParser
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("Unsupported platform")
#endif
import Foundation
@_spi(TestHelper)
import SwiftLintFramework
import SwiftyTextTable
extension SwiftLint {
struct Rules: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Display the list of rules and their identifiers")
@Option(help: "The path to a SwiftLint configuration file")
var config: String?
@OptionGroup
var rulesFilterOptions: RulesFilterOptions
@Flag(name: .shortAndLong, help: "Display full configuration details")
var verbose = false
@Argument(help: "The rule identifier to display description for")
var ruleID: String?
func run() throws {
if let ruleID {
guard let rule = primaryRuleList.list[ruleID] else {
throw SwiftLintError.usageError(description: "No rule with identifier: \(ruleID)")
}
rule.description.printDescription()
return
}
let configuration = Configuration(configurationFiles: [config].compactMap({ $0 }))
let rulesFilter = RulesFilter(enabledRules: configuration.rules)
let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions))
let table = TextTable(ruleList: rules, configuration: configuration, verbose: verbose)
print(table.render())
ExitHelper.successfullyExit()
}
}
}
private extension RuleDescription {
func printDescription() {
print("\(consoleDescription)")
guard !triggeringExamples.isEmpty else { return }
func indent(_ string: String) -> String {
return string.components(separatedBy: "\n")
.map { " \($0)" }
.joined(separator: "\n")
}
print("\nTriggering Examples (violation is marked with '↓'):")
for (index, example) in triggeringExamples.enumerated() {
print("\nExample #\(index + 1)\n\n\(indent(example.code))")
}
}
}
// MARK: - SwiftyTextTable
private extension TextTable {
init(ruleList: RuleList, configuration: Configuration, verbose: Bool) {
let columns = [
TextTableColumn(header: "identifier"),
TextTableColumn(header: "opt-in"),
TextTableColumn(header: "correctable"),
TextTableColumn(header: "enabled in your config"),
TextTableColumn(header: "kind"),
TextTableColumn(header: "analyzer"),
TextTableColumn(header: "uses sourcekit"),
TextTableColumn(header: "configuration")
]
self.init(columns: columns)
let sortedRules = ruleList.list.sorted { $0.0 < $1.0 }
func truncate(_ string: String) -> String {
let stringWithNoNewlines = string.replacingOccurrences(of: "\n", with: "\\n")
let minWidth = "configuration".count - "...".count
let configurationStartColumn = 140
let maxWidth = verbose ? Int.max : Terminal.currentWidth()
let truncatedEndIndex = stringWithNoNewlines.index(
stringWithNoNewlines.startIndex,
offsetBy: max(minWidth, maxWidth - configurationStartColumn),
limitedBy: stringWithNoNewlines.endIndex
)
if let truncatedEndIndex {
return stringWithNoNewlines[..<truncatedEndIndex] + "..."
}
return stringWithNoNewlines
}
for (ruleID, ruleType) in sortedRules {
let rule = ruleType.init()
let configuredRule = configuration.configuredRule(forID: ruleID)
addRow(values: [
ruleID,
(rule is OptInRule) ? "yes" : "no",
(rule is CorrectableRule) ? "yes" : "no",
configuredRule != nil ? "yes" : "no",
ruleType.description.kind.rawValue,
(rule is AnalyzerRule) ? "yes" : "no",
(rule is SourceKitFreeRule) ? "no" : "yes",
truncate((configuredRule ?? rule).configurationDescription)
])
}
}
}
private struct Terminal {
static func currentWidth() -> Int {
var size = winsize()
#if os(Linux)
_ = ioctl(CInt(STDOUT_FILENO), UInt(TIOCGWINSZ), &size)
#else
_ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)
#endif
return Int(size.ws_col)
}
}

View File

@@ -0,0 +1,26 @@
import ArgumentParser
import Foundation
@main
struct SwiftLint: AsyncParsableCommand {
static let configuration: CommandConfiguration = {
if let directory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] {
FileManager.default.changeCurrentDirectoryPath(directory)
}
return CommandConfiguration(
commandName: "swiftlint",
abstract: "A tool to enforce Swift style and conventions.",
version: Version.value,
subcommands: [
Analyze.self,
Docs.self,
GenerateDocs.self,
Lint.self,
Rules.self,
Version.self
],
defaultSubcommand: Lint.self
)
}()
}

View File

@@ -0,0 +1,23 @@
import ArgumentParser
import SwiftLintFramework
extension SwiftLint {
struct Version: ParsableCommand {
@Flag(help: "Display full version info")
var verbose = false
static let configuration = CommandConfiguration(abstract: "Display the current version of SwiftLint")
static var value: String { SwiftLintFramework.Version.current.value }
func run() throws {
if verbose, let buildID = ExecutableInfo.buildID {
print("Version:", Self.value)
print("Build ID:", buildID)
} else {
print(Self.value)
}
ExitHelper.successfullyExit()
}
}
}

View File

@@ -0,0 +1,283 @@
import CollectionConcurrencyKit
import Foundation
import SourceKittenFramework
import SwiftLintFramework
private actor CounterActor {
private var count = 0
func next() -> Int {
count += 1
return count
}
}
private func scriptInputFiles() throws -> [SwiftLintFile] {
let inputFileKey = "SCRIPT_INPUT_FILE_COUNT"
guard let countString = ProcessInfo.processInfo.environment[inputFileKey] else {
throw SwiftLintError.usageError(description: "\(inputFileKey) variable not set")
}
guard let count = Int(countString) else {
throw SwiftLintError.usageError(description: "\(inputFileKey) did not specify a number")
}
return (0..<count).compactMap { fileNumber in
do {
let environment = ProcessInfo.processInfo.environment
let variable = "SCRIPT_INPUT_FILE_\(fileNumber)"
guard let path = environment[variable] else {
throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)")
}
if path.bridge().isSwiftFile() {
return SwiftLintFile(pathDeferringReading: path)
}
return nil
} catch {
queuedPrintError(String(describing: error))
return nil
}
}
}
#if os(Linux)
private func autoreleasepool<T>(block: () -> T) -> T { return block() }
#endif
extension Configuration {
func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage) async throws -> [SwiftLintFile] {
let files = try await Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") {
try await getFiles(with: visitor)
}
let groupedFiles = try Signposts.record(name: "Configuration.VisitLintableFiles.GroupFiles") {
try groupFiles(files, visitor: visitor)
}
let lintersForFile = Signposts.record(name: "Configuration.VisitLintableFiles.LintersForFile") {
groupedFiles.map { file in
linters(for: [file.key: file.value], visitor: visitor)
}
}
let duplicateFileNames = Signposts.record(name: "Configuration.VisitLintableFiles.DuplicateFileNames") {
lintersForFile.map(\.duplicateFileNames)
}
let collected = await Signposts.record(name: "Configuration.VisitLintableFiles.Collect") {
await zip(lintersForFile, duplicateFileNames).asyncMap { linters, duplicateFileNames in
await collect(linters: linters, visitor: visitor, storage: storage,
duplicateFileNames: duplicateFileNames)
}
}
let result = await Signposts.record(name: "Configuration.VisitLintableFiles.Visit") {
await collected.asyncMap { linters, duplicateFileNames in
await visit(linters: linters, visitor: visitor, duplicateFileNames: duplicateFileNames)
}
}
return result.flatMap { $0 }
}
private func groupFiles(_ files: [SwiftLintFile], visitor: LintableFilesVisitor) throws
-> [Configuration: [SwiftLintFile]] {
if files.isEmpty && !visitor.allowZeroLintableFiles {
throw SwiftLintError.usageError(
description: "No lintable files found at paths: '\(visitor.paths.joined(separator: ", "))'"
)
}
var groupedFiles = [Configuration: [SwiftLintFile]]()
for file in files {
let fileConfiguration = configuration(for: file)
let fileConfigurationRootPath = fileConfiguration.rootDirectory.bridge()
// Files whose configuration specifies they should be excluded will be skipped
let shouldSkip = fileConfiguration.excludedPaths.contains { excludedRelativePath in
let excludedPath = fileConfigurationRootPath.appendingPathComponent(excludedRelativePath)
let filePathComponents = file.path?.bridge().pathComponents ?? []
let excludedPathComponents = excludedPath.bridge().pathComponents
return filePathComponents.starts(with: excludedPathComponents)
}
if !shouldSkip {
groupedFiles[fileConfiguration, default: []].append(file)
}
}
return groupedFiles
}
private func outputFilename(for path: String, duplicateFileNames: Set<String>) -> String {
let basename = path.bridge().lastPathComponent
if !duplicateFileNames.contains(basename) {
return basename
}
var pathComponents = path.bridge().pathComponents
for component in rootDirectory.bridge().pathComponents where pathComponents.first == component {
pathComponents.removeFirst()
}
return pathComponents.joined(separator: "/")
}
private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]],
visitor: LintableFilesVisitor) -> [Linter] {
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
var linters = [Linter]()
linters.reserveCapacity(fileCount)
for (config, files) in filesPerConfiguration {
let newConfig: Configuration
if visitor.cache != nil {
newConfig = config.withPrecomputedCacheDescription()
} else {
newConfig = config
}
linters += files.map { visitor.linter(forFile: $0, configuration: newConfig) }
}
return linters
}
private func collect(linters: [Linter],
visitor: LintableFilesVisitor,
storage: RuleStorage,
duplicateFileNames: Set<String>) async -> ([CollectedLinter], Set<String>) {
let counter = CounterActor()
let total = linters.filter(\.isCollecting).count
let progress = ProgressBar(count: total)
if visitor.showProgressBar && total > 0 {
await progress.initialize()
}
let collect = { (linter: Linter) -> CollectedLinter? in
let skipFile = visitor.shouldSkipFile(atPath: linter.file.path)
if !visitor.quiet && linter.isCollecting {
if visitor.showProgressBar {
await progress.printNext()
} else if let filePath = linter.file.path {
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
let collected = await counter.next()
if skipFile {
queuedPrintError("""
Skipping '\(outputFilename)' (\(collected)/\(total)) \
because its compiler arguments could not be found
""")
} else {
queuedPrintError("Collecting '\(outputFilename)' (\(collected)/\(total))")
}
}
}
guard !skipFile else {
return nil
}
return autoreleasepool {
linter.collect(into: storage)
}
}
let collectedLinters = await visitor.parallel ?
linters.concurrentCompactMap(collect) :
linters.asyncCompactMap(collect)
return (collectedLinters, duplicateFileNames)
}
private func visit(linters: [CollectedLinter],
visitor: LintableFilesVisitor,
duplicateFileNames: Set<String>) async -> [SwiftLintFile] {
let counter = CounterActor()
let progress = ProgressBar(count: linters.count)
if visitor.showProgressBar {
await progress.initialize()
}
let visit = { (linter: CollectedLinter) -> SwiftLintFile in
if !visitor.quiet {
if visitor.showProgressBar {
await progress.printNext()
} else if let filePath = linter.file.path {
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
let visited = await counter.next()
queuedPrintError("\(visitor.action) '\(outputFilename)' (\(visited)/\(linters.count))")
}
}
await Signposts.record(name: "Configuration.Visit", span: .file(linter.file.path ?? "")) {
await visitor.block(linter)
}
return linter.file
}
return await visitor.parallel ?
linters.concurrentMap(visit) :
linters.asyncMap(visit)
}
fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] {
if visitor.useSTDIN {
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
if let stdinString = String(data: stdinData, encoding: .utf8) {
return [SwiftLintFile(contents: stdinString)]
}
throw SwiftLintError.usageError(description: "stdin isn't a UTF8-encoded string")
} else if visitor.useScriptInputFiles {
let files = try scriptInputFiles()
guard visitor.forceExclude else {
return files
}
let scriptInputPaths = files.compactMap { $0.path }
let filesToLint = visitor.useExcludingByPrefix ?
filterExcludedPathsByPrefix(in: scriptInputPaths) :
filterExcludedPaths(in: scriptInputPaths)
return filesToLint.map(SwiftLintFile.init(pathDeferringReading:))
}
if !visitor.quiet {
let filesInfo: String
if visitor.paths.isEmpty || visitor.paths == [""] {
filesInfo = "in current working directory"
} else {
filesInfo = "at paths \(visitor.paths.joined(separator: ", "))"
}
queuedPrintError("\(visitor.action) Swift files \(filesInfo)")
}
return visitor.paths.flatMap {
self.lintableFiles(inPath: $0, forceExclude: visitor.forceExclude,
excludeByPrefix: visitor.useExcludingByPrefix)
}
}
func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil, storage: RuleStorage,
visitorBlock: @escaping (CollectedLinter) async -> Void) async throws -> [SwiftLintFile] {
let visitor = try LintableFilesVisitor.create(options, cache: cache,
allowZeroLintableFiles: allowZeroLintableFiles,
block: visitorBlock)
return try await visitLintableFiles(with: visitor, storage: storage)
}
// MARK: LintOrAnalyze Command
init(options: LintOrAnalyzeOptions) {
self.init(
configurationFiles: options.configurationFiles,
enableAllRules: options.enableAllRules,
cachePath: options.cachePath
)
}
}
private struct DuplicateCollector {
var all = Set<String>()
var duplicates = Set<String>()
}
private extension Collection where Element == Linter {
var duplicateFileNames: Set<String> {
let collector = reduce(into: DuplicateCollector()) { result, linter in
if let filename = linter.file.path?.bridge().lastPathComponent {
if result.all.contains(filename) {
result.duplicates.insert(filename)
}
result.all.insert(filename)
}
}
return collector.duplicates
}
}

View File

@@ -0,0 +1,28 @@
import Foundation
extension ProcessInfo {
var isLikelyXcodeCloudEnvironment: Bool {
// https://developer.apple.com/documentation/xcode/environment-variable-reference
let requiredKeys: Set = [
"CI",
"CI_BUILD_ID",
"CI_BUILD_NUMBER",
"CI_BUNDLE_ID",
"CI_COMMIT",
"CI_DERIVED_DATA_PATH",
"CI_PRODUCT",
"CI_PRODUCT_ID",
"CI_PRODUCT_PLATFORM",
"CI_PROJECT_FILE_PATH",
"CI_START_CONDITION",
"CI_TEAM_ID",
"CI_WORKFLOW",
"CI_WORKSPACE",
"CI_XCODE_PROJECT",
"CI_XCODE_SCHEME",
"CI_XCODEBUILD_ACTION"
]
return requiredKeys.isSubset(of: environment.keys)
}
}

View File

@@ -0,0 +1,52 @@
import Foundation
import SwiftLintFramework
struct BenchmarkEntry {
let id: String
let time: Double
}
struct Benchmark {
private let name: String
private var entries = [BenchmarkEntry]()
init(name: String) {
self.name = name
}
mutating func record(id: String, time: Double) {
guard id != "custom_rules" else { return }
entries.append(BenchmarkEntry(id: id, time: time))
}
mutating func record(file: SwiftLintFile, from start: Date) {
record(id: file.path ?? "<nopath>", time: -start.timeIntervalSinceNow)
}
func save() {
// Decomposed to improve compile times
let entriesDict: [String: Double] = entries.reduce(into: [String: Double]()) { accu, idAndTime in
accu[idAndTime.id] = (accu[idAndTime.id] ?? 0) + idAndTime.time
}
let entriesKeyValues: [(String, Double)] = entriesDict.sorted { $0.1 < $1.1 }
let lines: [String] = entriesKeyValues.map { id, time -> String in
return "\(numberFormatter.string(from: NSNumber(value: time))!): \(id)"
}
let string: String = lines.joined(separator: "\n") + "\n"
let url = URL(fileURLWithPath: "benchmark_\(name)_\(timestamp).txt", isDirectory: false)
try? string.data(using: .utf8)?.write(to: url, options: [.atomic])
}
}
private let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 3
return formatter
}()
private let timestamp: String = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy_MM_dd_HH_mm_ss"
return formatter.string(from: Date())
}()

View File

@@ -0,0 +1,108 @@
import Foundation
struct CompilerArgumentsExtractor {
static func allCompilerInvocations(compilerLogs: String) -> [[String]] {
var compilerInvocations = [[String]]()
compilerLogs.enumerateLines { line, _ in
if let swiftcIndex = line.range(of: "swiftc ")?.upperBound, line.contains(" -module-name ") {
let invocation = parseCLIArguments(String(line[swiftcIndex...]))
.expandingResponseFiles
.filteringCompilerArguments
compilerInvocations.append(invocation)
}
}
return compilerInvocations
}
}
// MARK: - Private
private func parseCLIArguments(_ string: String) -> [String] {
let escapedSpacePlaceholder = "\u{0}"
let scanner = Scanner(string: string)
var str = ""
var didStart = false
while let result = scanner.scanUpToString("\"") {
if didStart {
str += result.replacingOccurrences(of: " ", with: escapedSpacePlaceholder)
str += " "
} else {
str += result
}
_ = scanner.scanString("\"")
didStart.toggle()
}
return str.trimmingCharacters(in: .whitespaces)
.replacingOccurrences(of: "\\ ", with: escapedSpacePlaceholder)
.components(separatedBy: " ")
.map { $0.replacingOccurrences(of: escapedSpacePlaceholder, with: " ") }
}
/**
Partially filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: A tuple of partially filtered compiler arguments in `.0`, and whether or not there are
more flags to remove in `.1`.
*/
private func partiallyFilter(arguments args: [String]) -> ([String], Bool) {
guard let indexOfFlagToRemove = args.firstIndex(of: "-output-file-map") else {
return (args, false)
}
var args = args
args.remove(at: args.index(after: indexOfFlagToRemove))
args.remove(at: indexOfFlagToRemove)
return (args, true)
}
extension Array where Element == String {
/// Return the full list of compiler arguments, replacing any response files with their contents.
fileprivate var expandingResponseFiles: [String] {
return flatMap { arg -> [String] in
guard arg.starts(with: "@") else {
return [arg]
}
let responseFile = String(arg.dropFirst())
return (try? String(contentsOf: URL(fileURLWithPath: responseFile, isDirectory: false))).flatMap {
$0.trimmingCharacters(in: .newlines)
.components(separatedBy: "\n")
.expandingResponseFiles
} ?? [arg]
}
}
/// Returns filtered compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
var filteringCompilerArguments: [String] {
var args = self
if args.first == "swiftc" {
args.removeFirst()
}
// https://github.com/realm/SwiftLint/issues/3365
args = args.map { $0.replacingOccurrences(of: "\\=", with: "=") }
args = args.map { $0.replacingOccurrences(of: "\\ ", with: " ") }
args.append(contentsOf: ["-D", "DEBUG"])
var shouldContinueToFilterArguments = true
while shouldContinueToFilterArguments {
(args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args)
}
return args.filter {
![
"-parseable-output",
"-incremental",
"-serialize-diagnostics",
"-emit-dependencies",
"-use-frontend-parseable-output"
].contains($0)
}.map {
if $0 == "-O" {
return "-Onone"
} else if $0 == "-DNDEBUG=1" {
return "-DDEBUG=1"
}
return $0
}
}
}

View File

@@ -0,0 +1,12 @@
#if os(Linux)
import Glibc
#endif
enum ExitHelper {
static func successfullyExit() {
#if os(Linux)
// Workaround for https://github.com/apple/swift/issues/59961
Glibc.exit(0)
#endif
}
}

View File

@@ -0,0 +1,63 @@
import ArgumentParser
enum LeniencyOptions: String, EnumerableFlag {
case strict, lenient
static func help(for value: LeniencyOptions) -> ArgumentHelp? {
switch value {
case .strict:
return "Upgrades warnings to serious violations (errors)."
case .lenient:
return "Downgrades serious violations to warnings, warning threshold is disabled."
}
}
}
// MARK: - Common Arguments
struct LintOrAnalyzeArguments: ParsableArguments {
@Option(help: "The path to one or more SwiftLint configuration files, evaluated as a parent-child hierarchy.")
var config = [String]()
@Flag(name: [.long, .customLong("autocorrect")], help: "Correct violations whenever possible.")
var fix = false
@Flag(help: """
Should reformat the Swift files using the same mechanism used by Xcode (via SourceKit).
Only applied with `--fix`/`--autocorrect`.
""")
var format = false
@Flag(help: "Use an alternative algorithm to exclude paths for `excluded`, which may be faster in some cases.")
var useAlternativeExcluding = false
@Flag(help: "Read SCRIPT_INPUT_FILE* environment variables as files.")
var useScriptInputFiles = false
@Flag(exclusivity: .exclusive)
var leniency: LeniencyOptions?
@Flag(help: "Exclude files in config `excluded` even if their paths are explicitly specified.")
var forceExclude = false
@Flag(help: "Save benchmarks to `benchmark_files.txt` and `benchmark_rules.txt`.")
var benchmark = false
@Option(help: "The reporter used to log errors and warnings.")
var reporter: String?
@Flag(help: "Use the in-process version of SourceKit.")
var inProcessSourcekit = false
@Option(help: "The file where violations should be saved. Prints to stdout by default.")
var output: String?
@Flag(help: "Show a live-updating progress bar instead of each file being processed.")
var progress = false
}
// MARK: - Common Argument Help
// It'd be great to be able to parameterize an `@OptionGroup` so we could move these options into
// `LintOrAnalyzeArguments`.
func pathOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
ArgumentHelp(visibility: .hidden)
}
func pathsArgumentDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
"List of paths to the files or directories to \(mode.imperative)."
}
func quietOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
"Don't print status logs like '\(mode.verb.capitalized) <file>' & 'Done \(mode.verb)'."
}

View File

@@ -0,0 +1,342 @@
import Dispatch
import Foundation
@_spi(TestHelper)
import SwiftLintFramework
enum LintOrAnalyzeMode {
case lint, analyze
var imperative: String {
switch self {
case .lint:
return "lint"
case .analyze:
return "analyze"
}
}
var verb: String {
switch self {
case .lint:
return "linting"
case .analyze:
return "analyzing"
}
}
}
struct LintOrAnalyzeCommand {
static func run(_ options: LintOrAnalyzeOptions) async throws {
if options.inProcessSourcekit {
queuedPrintError(
"""
warning: The --in-process-sourcekit option is deprecated. \
SwiftLint now always uses an in-process SourceKit.
"""
)
}
try await Signposts.record(name: "LintOrAnalyzeCommand.run") {
try await options.autocorrect ? autocorrect(options) : lintOrAnalyze(options)
}
ExitHelper.successfullyExit()
}
private static func lintOrAnalyze(_ options: LintOrAnalyzeOptions) async throws {
let builder = LintOrAnalyzeResultBuilder(options)
let files = try await collectViolations(builder: builder)
try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") {
try postProcessViolations(files: files, builder: builder)
}
}
private static func collectViolations(builder: LintOrAnalyzeResultBuilder) async throws -> [SwiftLintFile] {
let options = builder.options
let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation")
return try await builder.configuration.visitLintableFiles(options: options, cache: builder.cache,
storage: builder.storage) { linter in
let currentViolations: [StyleViolation]
if options.benchmark {
CustomRuleTimer.shared.activate()
let start = Date()
let (violationsBeforeLeniency, currentRuleTimes) = linter
.styleViolationsAndRuleTimes(using: builder.storage)
currentViolations = applyLeniency(options: options, violations: violationsBeforeLeniency)
visitorMutationQueue.sync {
builder.fileBenchmark.record(file: linter.file, from: start)
currentRuleTimes.forEach { builder.ruleBenchmark.record(id: $0, time: $1) }
builder.violations += currentViolations
}
} else {
currentViolations = applyLeniency(options: options,
violations: linter.styleViolations(using: builder.storage))
visitorMutationQueue.sync {
builder.violations += currentViolations
}
}
linter.file.invalidateCache()
builder.report(violations: currentViolations, realtimeCondition: true)
}
}
private static func postProcessViolations(files: [SwiftLintFile], builder: LintOrAnalyzeResultBuilder) throws {
let options = builder.options
let configuration = builder.configuration
if isWarningThresholdBroken(configuration: configuration, violations: builder.violations)
&& !options.lenient {
builder.violations.append(
createThresholdViolation(threshold: configuration.warningThreshold!)
)
builder.report(violations: [builder.violations.last!], realtimeCondition: true)
}
builder.report(violations: builder.violations, realtimeCondition: false)
let numberOfSeriousViolations = builder.violations.filter({ $0.severity == .error }).count
if !options.quiet {
printStatus(violations: builder.violations, files: files, serious: numberOfSeriousViolations,
verb: options.verb)
}
if options.benchmark {
builder.fileBenchmark.save()
for (id, time) in CustomRuleTimer.shared.dump() {
builder.ruleBenchmark.record(id: id, time: time)
}
builder.ruleBenchmark.save()
if !options.quiet, let memoryUsage = memoryUsage() {
queuedPrintError(memoryUsage)
}
}
try builder.cache?.save()
guard numberOfSeriousViolations == 0 else { exit(2) }
}
private static func printStatus(violations: [StyleViolation], files: [SwiftLintFile], serious: Int, verb: String) {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError(
"Done \(verb)! Found \(violations.count) violation\(pluralSuffix(violations)), " +
"\(serious) serious in \(files.count) file\(pluralSuffix(files))."
)
}
private static func isWarningThresholdBroken(configuration: Configuration,
violations: [StyleViolation]) -> Bool {
guard let warningThreshold = configuration.warningThreshold else { return false }
let numberOfWarningViolations = violations.filter({ $0.severity == .warning }).count
return numberOfWarningViolations >= warningThreshold
}
private static func createThresholdViolation(threshold: Int) -> StyleViolation {
let description = RuleDescription(
identifier: "warning_threshold",
name: "Warning Threshold",
description: "Number of warnings thrown is above the threshold",
kind: .lint
)
return StyleViolation(
ruleDescription: description,
severity: .error,
location: Location(file: "", line: 0, character: 0),
reason: "Number of warnings exceeded threshold of \(threshold).")
}
private static func applyLeniency(options: LintOrAnalyzeOptions, violations: [StyleViolation]) -> [StyleViolation] {
switch (options.lenient, options.strict) {
case (false, false):
return violations
case (true, false):
return violations.map {
if $0.severity == .error {
return $0.with(severity: .warning)
} else {
return $0
}
}
case (false, true):
return violations.map {
if $0.severity == .warning {
return $0.with(severity: .error)
} else {
return $0
}
}
case (true, true):
queuedFatalError("Invalid command line options: 'lenient' and 'strict' are mutually exclusive.")
}
}
private static func autocorrect(_ options: LintOrAnalyzeOptions) async throws {
let storage = RuleStorage()
let configuration = Configuration(options: options)
let correctionsBuilder = CorrectionsBuilder()
let files = try await configuration
.visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
if options.format {
switch configuration.indentation {
case .tabs:
linter.format(useTabs: true, indentWidth: 4)
case .spaces(let count):
linter.format(useTabs: false, indentWidth: count)
}
}
let corrections = linter.correct(using: storage)
if !corrections.isEmpty && !options.quiet {
if options.useSTDIN {
queuedPrint(linter.file.contents)
} else {
if options.progress {
await correctionsBuilder.append(corrections)
} else {
let correctionLogs = corrections.map(\.consoleDescription)
queuedPrint(correctionLogs.joined(separator: "\n"))
}
}
}
}
if !options.quiet {
if options.progress {
let corrections = await correctionsBuilder.corrections
if !corrections.isEmpty {
let correctionLogs = corrections.map(\.consoleDescription)
options.writeToOutput(correctionLogs.joined(separator: "\n"))
}
}
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
}
}
}
struct LintOrAnalyzeOptions {
let mode: LintOrAnalyzeMode
let paths: [String]
let useSTDIN: Bool
let configurationFiles: [String]
let strict: Bool
let lenient: Bool
let forceExclude: Bool
let useExcludingByPrefix: Bool
let useScriptInputFiles: Bool
let benchmark: Bool
let reporter: String?
let quiet: Bool
let output: String?
let progress: Bool
let cachePath: String?
let ignoreCache: Bool
let enableAllRules: Bool
let autocorrect: Bool
let format: Bool
let compilerLogPath: String?
let compileCommands: String?
let inProcessSourcekit: Bool
var verb: String {
if autocorrect {
return "correcting"
} else {
return mode.verb
}
}
}
private class LintOrAnalyzeResultBuilder {
var fileBenchmark = Benchmark(name: "files")
var ruleBenchmark = Benchmark(name: "rules")
var violations = [StyleViolation]()
let storage = RuleStorage()
let configuration: Configuration
let reporter: Reporter.Type
let cache: LinterCache?
let options: LintOrAnalyzeOptions
init(_ options: LintOrAnalyzeOptions) {
let config = Signposts.record(name: "LintOrAnalyzeCommand.ParseConfiguration") {
Configuration(options: options)
}
configuration = config
reporter = reporterFrom(identifier: options.reporter ?? config.reporter)
if options.ignoreCache || ProcessInfo.processInfo.isLikelyXcodeCloudEnvironment {
cache = nil
} else {
cache = LinterCache(configuration: config)
}
self.options = options
if let outFile = options.output {
do {
try Data().write(to: URL(fileURLWithPath: outFile))
} catch {
queuedPrintError("Could not write to file at path \(outFile)")
}
}
}
func report(violations: [StyleViolation], realtimeCondition: Bool) {
if (reporter.isRealtime && (!options.progress || options.output != nil)) == realtimeCondition {
let report = reporter.generateReport(violations)
if !report.isEmpty {
options.writeToOutput(report)
}
}
}
}
private extension LintOrAnalyzeOptions {
func writeToOutput(_ string: String) {
guard let outFile = output else {
queuedPrint(string)
return
}
do {
let outFileURL = URL(fileURLWithPath: outFile)
let fileUpdater = try FileHandle(forUpdating: outFileURL)
fileUpdater.seekToEndOfFile()
fileUpdater.write(Data((string + "\n").utf8))
fileUpdater.closeFile()
} catch {
queuedPrintError("Could not write to file at path \(outFile)")
}
}
}
private actor CorrectionsBuilder {
private(set) var corrections: [Correction] = []
func append(_ corrections: [Correction]) {
self.corrections.append(contentsOf: corrections)
}
}
private func memoryUsage() -> String? {
#if os(Linux)
return nil
#else
var info = mach_task_basic_info()
let basicInfoCount = MemoryLayout<mach_task_basic_info>.stride / MemoryLayout<natural_t>.stride
var count = mach_msg_type_number_t(basicInfoCount)
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: basicInfoCount) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if kerr == KERN_SUCCESS {
let bytes = Measurement<UnitInformationStorage>(value: Double(info.resident_size), unit: .bytes)
let formatted = ByteCountFormatter().string(from: bytes)
return "Memory used: \(formatted)"
} else {
let errorMessage = String(cString: mach_error_string(kerr), encoding: .ascii)
return "Error with task_info(): \(errorMessage ?? "unknown")"
}
#endif
}

View File

@@ -0,0 +1,253 @@
import Foundation
import SourceKittenFramework
import SwiftLintFramework
typealias File = String
typealias Arguments = [String]
class CompilerInvocations {
static func buildLog(compilerInvocations: [[String]]) -> CompilerInvocations {
return ArrayCompilerInvocations(invocations: compilerInvocations)
}
static func compilationDatabase(compileCommands: [File: Arguments]) -> CompilerInvocations {
return CompilationDatabaseInvocations(compileCommands: compileCommands)
}
/// Default implementation
func arguments(forFile path: String?) -> Arguments { [] }
// MARK: - Private
private class ArrayCompilerInvocations: CompilerInvocations {
private let invocationsByArgument: [String: [Arguments]]
init(invocations: [Arguments]) {
// Store invocations by the path, so next when we'll be asked for arguments,
// we'll be able to return them faster
self.invocationsByArgument = invocations.reduce(into: [:]) { result, arguments in
arguments.forEach { result[$0, default: []].append(arguments) }
}
}
override func arguments(forFile path: String?) -> Arguments {
return path.flatMap { path in
return invocationsByArgument[path]?.first
} ?? []
}
}
private class CompilationDatabaseInvocations: CompilerInvocations {
private let compileCommands: [File: Arguments]
init(compileCommands: [File: Arguments]) {
self.compileCommands = compileCommands
}
override func arguments(forFile path: String?) -> Arguments {
return path.flatMap { path in
return compileCommands[path] ??
compileCommands[path.path(relativeTo: FileManager.default.currentDirectoryPath)]
} ?? []
}
}
}
enum LintOrAnalyzeModeWithCompilerArguments {
case lint
case analyze(allCompilerInvocations: CompilerInvocations)
}
private func resolveParamsFiles(args: [String]) -> [String] {
return args.reduce(into: []) { (allArgs: inout [String], arg: String) -> Void in
if arg.hasPrefix("@"), let contents = try? String(contentsOfFile: String(arg.dropFirst())) {
allArgs.append(contentsOf: resolveParamsFiles(args: contents.split(separator: "\n").map(String.init)))
} else {
allArgs.append(arg)
}
}
}
struct LintableFilesVisitor {
let paths: [String]
let action: String
let useSTDIN: Bool
let quiet: Bool
let showProgressBar: Bool
let useScriptInputFiles: Bool
let forceExclude: Bool
let useExcludingByPrefix: Bool
let cache: LinterCache?
let parallel: Bool
let allowZeroLintableFiles: Bool
let mode: LintOrAnalyzeModeWithCompilerArguments
let block: (CollectedLinter) async -> Void
private init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, showProgressBar: Bool,
useScriptInputFiles: Bool, forceExclude: Bool, useExcludingByPrefix: Bool,
cache: LinterCache?, compilerInvocations: CompilerInvocations?,
allowZeroLintableFiles: Bool, block: @escaping (CollectedLinter) async -> Void) {
self.paths = resolveParamsFiles(args: paths)
self.action = action
self.useSTDIN = useSTDIN
self.quiet = quiet
self.showProgressBar = showProgressBar
self.useScriptInputFiles = useScriptInputFiles
self.forceExclude = forceExclude
self.useExcludingByPrefix = useExcludingByPrefix
self.cache = cache
if let compilerInvocations {
self.mode = .analyze(allCompilerInvocations: compilerInvocations)
// SourceKit had some changes in 5.6 that makes it ~100x more expensive
// to process files concurrently. By processing files serially, it's
// only 2x slower than before.
self.parallel = SwiftVersion.current < .fiveDotSix
} else {
self.mode = .lint
self.parallel = true
}
self.block = block
self.allowZeroLintableFiles = allowZeroLintableFiles
}
static func create(_ options: LintOrAnalyzeOptions,
cache: LinterCache?,
allowZeroLintableFiles: Bool,
block: @escaping (CollectedLinter) async -> Void)
throws -> LintableFilesVisitor {
try Signposts.record(name: "LintableFilesVisitor.Create") {
let compilerInvocations: CompilerInvocations?
if options.mode == .lint {
compilerInvocations = nil
} else {
compilerInvocations = try loadCompilerInvocations(options)
}
return LintableFilesVisitor(
paths: options.paths, action: options.verb.bridge().capitalized,
useSTDIN: options.useSTDIN, quiet: options.quiet,
showProgressBar: options.progress,
useScriptInputFiles: options.useScriptInputFiles,
forceExclude: options.forceExclude,
useExcludingByPrefix: options.useExcludingByPrefix,
cache: cache,
compilerInvocations: compilerInvocations,
allowZeroLintableFiles: allowZeroLintableFiles, block: block
)
}
}
func shouldSkipFile(atPath path: String?) -> Bool {
switch self.mode {
case .lint:
return false
case let .analyze(compilerInvocations):
let compilerArguments = compilerInvocations.arguments(forFile: path)
return compilerArguments.isEmpty
}
}
func linter(forFile file: SwiftLintFile, configuration: Configuration) -> Linter {
switch self.mode {
case .lint:
return Linter(file: file, configuration: configuration, cache: cache)
case let .analyze(compilerInvocations):
let compilerArguments = compilerInvocations.arguments(forFile: file.path)
return Linter(file: file, configuration: configuration, compilerArguments: compilerArguments)
}
}
private static func loadCompilerInvocations(_ options: LintOrAnalyzeOptions) throws -> CompilerInvocations {
if let path = options.compilerLogPath {
guard let compilerInvocations = self.loadLogCompilerInvocations(path) else {
throw SwiftLintError.usageError(description: "Could not read compiler log at path: '\(path)'")
}
return .buildLog(compilerInvocations: compilerInvocations)
} else if let path = options.compileCommands {
do {
return .compilationDatabase(compileCommands: try self.loadCompileCommands(path))
} catch {
throw SwiftLintError.usageError(
description: "Could not read compilation database at path: '\(path)' \(error.localizedDescription)"
)
}
}
throw SwiftLintError.usageError(description: "Could not read compiler invocations")
}
private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? {
if let data = FileManager.default.contents(atPath: path),
let logContents = String(data: data, encoding: .utf8) {
if logContents.isEmpty {
return nil
}
return CompilerArgumentsExtractor.allCompilerInvocations(compilerLogs: logContents)
}
return nil
}
private static func loadCompileCommands(_ path: String) throws -> [File: Arguments] {
guard let fileContents = FileManager.default.contents(atPath: path) else {
throw CompileCommandsLoadError.nonExistentFile(path)
}
if path.hasSuffix(".yaml") || path.hasSuffix(".yml") {
// Assume this is a SwiftPM yaml file
return try SwiftPMCompilationDB.parse(yaml: fileContents)
}
guard let object = try? JSONSerialization.jsonObject(with: fileContents),
let compileDB = object as? [[String: Any]] else {
throw CompileCommandsLoadError.malformedCommands(path)
}
// Convert the compilation database to a dictionary, with source files as keys and compiler arguments as values.
//
// Compilation databases are an array of dictionaries. Each dict has "file" and "arguments" keys.
var commands = [File: Arguments]()
for (index, entry) in compileDB.enumerated() {
guard let file = entry["file"] as? String else {
throw CompileCommandsLoadError.malformedFile(path, index)
}
guard let arguments = entry["arguments"] as? [String] else {
throw CompileCommandsLoadError.malformedArguments(path, index)
}
guard arguments.contains(file) else {
throw CompileCommandsLoadError.missingFileInArguments(path, index, arguments)
}
commands[file] = arguments.filteringCompilerArguments
}
return commands
}
}
private enum CompileCommandsLoadError: LocalizedError {
case nonExistentFile(String)
case malformedCommands(String)
case malformedFile(String, Int)
case malformedArguments(String, Int)
case missingFileInArguments(String, Int, [String])
var errorDescription: String? {
switch self {
case let .nonExistentFile(path):
return "Could not read compile commands file at '\(path)'"
case let .malformedCommands(path):
return "Compile commands file at '\(path)' isn't in the correct format"
case let .malformedFile(path, index):
return "Missing or invalid (must be a string) 'file' key in \(path) at index \(index)"
case let .malformedArguments(path, index):
return "Missing or invalid (must be an array of strings) 'arguments' key in \(path) at index \(index)"
case let .missingFileInArguments(path, index, arguments):
return "Entry in \(path) at index \(index) has 'arguments' which do not contain the 'file': \(arguments)"
}
}
}

View File

@@ -0,0 +1,63 @@
import Dispatch
import Foundation
import SwiftLintFramework
// Inspired by https://github.com/jkandzi/Progress.swift
actor ProgressBar {
private var index = 1
private var lastPrintedTime: TimeInterval = 0.0
private let startTime = uptime()
private let count: Int
init(count: Int) {
self.count = count
}
func initialize() {
// When progress is printed, the previous line is reset, so print an empty line before anything else
queuedPrintError("")
}
func printNext() {
guard index <= count else { return }
let currentTime = uptime()
if currentTime - lastPrintedTime > 0.1 || index == count {
let lineReset = "\u{1B}[1A\u{1B}[K"
let bar = makeBar()
let timeEstimate = makeTimeEstimate(currentTime: currentTime)
let lineContents = "\(index) of \(count) \(bar) \(timeEstimate)"
queuedPrintError("\(lineReset)\(lineContents)")
lastPrintedTime = currentTime
}
index += 1
}
// MARK: - Private
private func makeBar() -> String {
let barLength = 30
let completedBarElements = Int(Double(barLength) * (Double(index) / Double(count)))
let barArray = Array(repeating: "=", count: completedBarElements) +
Array(repeating: " ", count: barLength - completedBarElements)
return "[\(barArray.joined())]"
}
private func makeTimeEstimate(currentTime: TimeInterval) -> String {
let totalTime = currentTime - startTime
let itemsPerSecond = Double(index) / totalTime
let estimatedTimeRemaining = Double(count - index) / itemsPerSecond
let estimatedTimeRemainingString = "\(Int(estimatedTimeRemaining))s"
return "ETA: \(estimatedTimeRemainingString) (\(Int(itemsPerSecond)) files/s)"
}
}
#if os(Linux)
// swiftlint:disable:next identifier_name
private let NSEC_PER_SEC = 1_000_000_000
#endif
private func uptime() -> TimeInterval {
Double(DispatchTime.now().uptimeNanoseconds) / Double(NSEC_PER_SEC)
}

View File

@@ -0,0 +1,49 @@
@_spi(TestHelper)
import SwiftLintFramework
extension RulesFilter {
struct ExcludingOptions: OptionSet {
let rawValue: Int
static let enabled = Self(rawValue: 1 << 0)
static let disabled = Self(rawValue: 1 << 1)
static let uncorrectable = Self(rawValue: 1 << 2)
}
}
class RulesFilter {
private let allRules: RuleList
private let enabledRules: [Rule]
init(allRules: RuleList = primaryRuleList, enabledRules: [Rule]) {
self.allRules = allRules
self.enabledRules = enabledRules
}
func getRules(excluding excludingOptions: ExcludingOptions) -> RuleList {
if excludingOptions.isEmpty {
return allRules
}
let filtered: [Rule.Type] = allRules.list.compactMap { ruleID, ruleType in
let enabledRule = enabledRules.first { rule in
type(of: rule).description.identifier == ruleID
}
let isRuleEnabled = enabledRule != nil
if excludingOptions.contains(.enabled) && isRuleEnabled {
return nil
}
if excludingOptions.contains(.disabled) && !isRuleEnabled {
return nil
}
if excludingOptions.contains(.uncorrectable) && !(ruleType is CorrectableRule.Type) {
return nil
}
return ruleType
}
return RuleList(rules: filtered)
}
}

View File

@@ -0,0 +1,73 @@
#if canImport(os)
import os.signpost
private let timelineLog = OSLog(subsystem: "io.realm.swiftlint", category: "Timeline")
private let fileLog = OSLog(subsystem: "io.realm.swiftlint", category: "File")
#endif
struct Signposts {
enum Span {
case timeline, file(String)
}
static func record<R>(name: StaticString, span: Span = .timeline, body: () throws -> R) rethrows -> R {
#if canImport(os)
let log: OSLog
let description: String?
switch span {
case .timeline:
log = timelineLog
description = nil
case .file(let file):
log = fileLog
description = file
}
let signpostID = OSSignpostID(log: log)
if let description {
os_signpost(.begin, log: log, name: name, signpostID: signpostID, "%{public}s", description)
} else {
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
}
let result = try body()
if let description {
os_signpost(.end, log: log, name: name, signpostID: signpostID, "%{public}s", description)
} else {
os_signpost(.end, log: log, name: name, signpostID: signpostID)
}
return result
#else
return try body()
#endif
}
static func record<R>(name: StaticString, span: Span = .timeline, body: () async throws -> R) async rethrows -> R {
#if canImport(os)
let log: OSLog
let description: String?
switch span {
case .timeline:
log = timelineLog
description = nil
case .file(let file):
log = fileLog
description = file
}
let signpostID = OSSignpostID(log: log)
if let description {
os_signpost(.begin, log: log, name: name, signpostID: signpostID, "%{public}s", description)
} else {
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
}
let result = try await body()
if let description {
os_signpost(.end, log: log, name: name, signpostID: signpostID, "%{public}s", description)
} else {
os_signpost(.end, log: log, name: name, signpostID: signpostID)
}
return result
#else
return try await body()
#endif
}
}

View File

@@ -0,0 +1,12 @@
import Foundation
enum SwiftLintError: LocalizedError {
case usageError(description: String)
var errorDescription: String? {
switch self {
case .usageError(let description):
return description
}
}
}

View File

@@ -0,0 +1,74 @@
import Foundation
import Yams
private struct SwiftPMCommand: Codable {
let tool: String
let module: String?
let sources: [String]?
let args: [String]?
let importPaths: [String]?
enum CodingKeys: String, CodingKey {
case tool
case module = "module-name"
case sources
case args = "other-args"
case importPaths = "import-paths"
}
}
private struct SwiftPMNode: Codable {}
private struct SwiftPMNodes: Codable {
let nodes: [String: SwiftPMNode]
}
struct SwiftPMCompilationDB: Codable {
private let commands: [String: SwiftPMCommand]
static func parse(yaml: Data) throws -> [File: Arguments] {
let decoder = YAMLDecoder()
let compilationDB: SwiftPMCompilationDB
if ProcessInfo.processInfo.environment["TEST_SRCDIR"] != nil {
// Running tests
let nodes = try decoder.decode(SwiftPMNodes.self, from: yaml)
let suffix = "/Source/swiftlint/"
let pathToReplace = Array(nodes.nodes.keys.filter({ node in
node.hasSuffix(suffix)
}))[0].dropLast(suffix.count - 1)
let stringFileContents = String(data: yaml, encoding: .utf8)!
.replacingOccurrences(of: pathToReplace, with: "")
compilationDB = try decoder.decode(Self.self, from: stringFileContents)
} else {
compilationDB = try decoder.decode(Self.self, from: yaml)
}
let swiftCompilerCommands = compilationDB.commands
.filter { $0.value.tool == "swift-compiler" }
let allSwiftSources = swiftCompilerCommands
.flatMap { $0.value.sources ?? [] }
.filter { $0.hasSuffix(".swift") }
return Dictionary(uniqueKeysWithValues: allSwiftSources.map { swiftSource in
let command = swiftCompilerCommands
.values
.first { $0.sources?.contains(swiftSource) == true }
guard let command,
let module = command.module,
let sources = command.sources,
let arguments = command.args,
let importPaths = command.importPaths
else {
return (swiftSource, [])
}
let args = ["-module-name", module] +
sources +
arguments.filteringCompilerArguments +
["-I"] + importPaths
return (swiftSource, args)
})
}
}

View File

@@ -0,0 +1,59 @@
//
// BuildSettings.swift
// Cargo
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
//set -eu;
//
//BUILT_SRC="./em_proxy/$LIB_FILE_NAME.a"
//ln -f -- "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" || cp "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
//echo "$BUILT_SRC -> $TARGET_BUILD_DIR/$EXECUTABLE_PATH"
//# generated with cargo-xcode 1.5.0
//# modified to use prebuilt binaries
//
//set -eu;
//
//BUILT_SRC="./minimuxer/$LIB_FILE_NAME.a"
//ln -f -- "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" || cp "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
//echo "$BUILT_SRC -> $TARGET_BUILD_DIR/$EXECUTABLE_PATH"
//
//# xcode generates dep file, but for its own path, so append our rename to it
// #DEP_FILE_SRC="minimuxer/target/${CARGO_XCODE_TARGET_TRIPLE}/release/${CARGO_XCODE_CARGO_DEP_FILE_NAME}"
// #if [ -f "$DEP_FILE_SRC" ]; then
//# DEP_FILE_DST="${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d"
//# cp -f "$DEP_FILE_SRC" "$DEP_FILE_DST"
//# echo >> "$DEP_FILE_DST" "$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC"
//#fi
//
//# lipo script needs to know all the platform-specific files that have been built
//# archs is in the file name, so that paths don't stay around after archs change
//# must match input for LipoScript
// #FILE_LIST="${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist"
// #touch "$FILE_LIST"
// #if ! egrep -q "$SCRIPT_OUTPUT_FILE_0" "$FILE_LIST" ; then
//# echo >> "$FILE_LIST" "$SCRIPT_OUTPUT_FILE_0"
//#fi
import ArgumentParser
/// A representation of a build setting in an Xcode project, e.g.
/// `IPHONEOS_DEPLOYMENT_TARGET=13.0`
struct BuildSetting: ExpressibleByArgument {
/// The name of the build setting, e.g. `IPHONEOS_DEPLOYMENT_TARGET`
let name: String
/// The value of the build setting
let value: String
init?(argument: String) {
let components = argument.components(separatedBy: "=")
guard components.count == 2 else { return nil }
name = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
value = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@@ -0,0 +1,71 @@
//
// Command+Options.swift
// Cargo
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
import ArgumentParser
import PackageModel
extension Command {
struct Options: ParsableArguments {
// MARK: - Package Loading
@Option(help: ArgumentHelp("The location of the Package", valueName: "directory"))
var packagePath = "."
// MARK: - Building
@Option(help: ArgumentHelp("The location of the build/cache directory to use", valueName: "directory"))
var buildPath = ".build"
@Option(help: ArgumentHelp("Build with a specific configuration", valueName: "debug|release"))
var configuration = PackageModel.BuildConfiguration.release
@Flag(inversion: .prefixedNo, help: "Whether to clean before we build")
var clean = true
@Flag(inversion: .prefixedNo, help: "Whether to include debug symbols in the built XCFramework")
var debugSymbols = true
@Flag(help: "Prints the available products and targets")
var listProducts = false
@Option(help: "The path to a .xcconfig file that can be used to override Xcode build settings. Relative to the package path.")
var xcconfig: String?
@Flag(help: "Enables Library Evolution for the whole build stack. Normally we apply it only to the targets listed to be built to work around issues with projects that don't support it.")
var stackEvolution: Bool = false
@Option(help: ArgumentHelp("Arbitrary Xcode build settings that are passed directly to the `xcodebuild` invocation. Can be specified multiple times.", valueName: "NAME=VALUE"))
var xcSetting: [BuildSetting] = []
// MARK: - Output Options
@Option(
help: ArgumentHelp(
"A list of platforms you want to build for. Can be specified multiple times."
+ " Default is to build for all platforms supported in your Package.swift, or all Apple platforms (except for maccatalyst platform) if omitted",
valueName: TargetPlatform.allCases.map { $0.rawValue }.joined(separator: "|")
)
)
var platform: [TargetPlatform] = []
@Option(help: ArgumentHelp("Where to place the compiled library", valueName: "directory"))
var output = "."
@Flag(help: .hidden)
var githubAction: Bool = false
// MARK: - Targets
@Argument(help: "An optional list of products (or targets) to build. Defaults to building all `.library` products")
var products: [String] = []
}
}
// MARK: - ParsableArguments Extensions
extension PackageModel.BuildConfiguration: ExpressibleByArgument {}

View File

@@ -0,0 +1,142 @@
//
// Command.swift
// Cargo
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
import ArgumentParser
import Foundation
import PackageLoading
import PackageModel
import TSCBasic
import Workspace
import Xcodeproj
struct Command: ParsableCommand {
// MARK: - Configuration
static var configuration = CommandConfiguration(
abstract: "Builds a `rust` package using `cargo`.",
discussion:
"""
""",
version: "1.1.0"
)
// MARK: - Arguments
@OptionGroup()
var options: Options
// MARK: - Execution
// swiftlint:disable:next function_body_length
func run() throws {
// load all/validate of the package info
let package = try PackageInfo(options: options)
// validate that package to make sure we can generate it
let validation = package.validationErrors()
if validation.isEmpty == false {
for error in validation {
print(error.isFatal ? "Error:" : "Warning:", error.errorDescription!)
}
if validation.contains(where: { $0.isFatal }) {
Darwin.exit(1)
}
}
// generate the Xcode project file
let generator = ProjectGenerator(package: package)
let platforms = try package.supportedPlatforms()
// get what we're building
try generator.writeDistributionXcconfig()
let project = try generator.generate()
// printing packages?
if options.listProducts {
package.printAllProducts(project: project)
Darwin.exit(0)
}
// get valid packages and their SDKs
let productNames = try package.validProductNames(project: project)
let sdks = platforms.flatMap { $0.sdks }
// we've applied the xcconfig to everything, but some dependencies (*cough* swift-nio)
// have build errors, so we remove it from targets we're not building
if options.stackEvolution == false {
try project.enableDistribution(targets: productNames, xcconfig: AbsolutePath(package.distributionBuildXcconfig.path).relative(to: AbsolutePath(package.rootDirectory.path)))
}
// save the project
try project.save(to: generator.projectPath)
// start building
let builder = XcodeBuilder(project: project, projectPath: generator.projectPath, package: package, options: options)
// clean first
if options.clean {
try builder.clean()
}
// all of our targets for each platform, then group the resulting .frameworks by target
var frameworkFiles: [String: [XcodeBuilder.BuildResult]] = [:]
for sdk in sdks {
try builder.build(targets: productNames, sdk: sdk)
.forEach { pair in
if frameworkFiles[pair.key] == nil {
frameworkFiles[pair.key] = []
}
frameworkFiles[pair.key]?.append(pair.value)
}
}
var xcframeworkFiles: [(String, Foundation.URL)] = []
// then we merge the resulting frameworks
try frameworkFiles
.forEach { pair in
xcframeworkFiles.append((pair.key, try builder.merge(target: pair.key, buildResults: pair.value)))
}
// zip it up if thats what they want
if options.zip {
let zipper = Zipper(package: package)
let zipped = try xcframeworkFiles
.flatMap { pair -> [Foundation.URL] in
let zip = try zipper.zip(target: pair.0, version: self.options.zipVersion, file: pair.1)
let checksum = try zipper.checksum(file: zip)
try zipper.clean(file: pair.1)
return [zip, checksum]
}
// notify the action if we have one
if options.githubAction {
let zips = zipped.map { $0.path }.joined(separator: "\n")
let data = Data(zips.utf8)
let url = Foundation.URL(fileURLWithPath: options.buildPath).appendingPathComponent("xcframework-zipfile.url")
try data.write(to: url)
}
}
}
}
// MARK: - Errors
private enum Error: Swift.Error, LocalizedError {
case noProducts
var errorDescription: String? {
switch self {
case .noProducts: return ""
}
}
}

View File

@@ -0,0 +1,12 @@
//
// Collection-Extensions.swift
// swift-create-xcframework
//
// Created by Rob Amos on 9/5/20.
//
extension Collection {
var nonEmpty: Self? {
isEmpty ? nil : self
}
}

View File

@@ -0,0 +1,27 @@
//
// PackageDescription+Extensions.swift
// swift-create-xcframework
//
// Created by Rob Amos on 7/5/20.
//
import PackageModel
extension ProductType {
var isLibrary: Bool {
if case .library = self {
return true
}
return false
}
}
extension Manifest {
var libraryProductNames: [String] {
products
.compactMap { product in
guard product.type.isLibrary else { return nil }
return product.name
}
}
}

View File

@@ -0,0 +1,113 @@
//
// Platforms.swift
// Cargo
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
import ArgumentParser
import PackageModel
enum TargetPlatform: String, ExpressibleByArgument, CaseIterable {
case ios
case macos
case maccatalyst
case tvos
case watchos
init?(argument: String) {
self.init(rawValue: argument.lowercased())
}
var platformName: String {
switch self {
case .ios: "ios"
case .macos: "macos"
case .maccatalyst: "macos"
case .tvos: "tvos"
case .watchos: "watchos"
}
}
// MARK: - Target SDKs
struct SDK {
let destination: String
let archiveName: String
let releaseFolder: String
let buildSettings: [String: String]?
}
var sdks: [SDK] {
switch self {
case .ios:
return [
SDK(
destination: "generic/platform=iOS",
archiveName: "iphoneos.xcarchive",
releaseFolder: "Release-iphoneos",
buildSettings: nil
),
SDK(
destination: "generic/platform=iOS Simulator",
archiveName: "iphonesimulator.xcarchive",
releaseFolder: "Release-iphonesimulator",
buildSettings: nil
)
]
case .macos:
return [
SDK(
destination: "generic/platform=macOS,name=Any Mac",
archiveName: "macos.xcarchive",
releaseFolder: "Release",
buildSettings: nil
)
]
case .maccatalyst:
return [
SDK(
destination: "generic/platform=macOS,variant=Mac Catalyst",
archiveName: "maccatalyst.xcarchive",
releaseFolder: "Release-maccatalyst",
buildSettings: ["SUPPORTS_MACCATALYST": "YES"]
)
]
case .tvos:
return [
SDK(
destination: "generic/platform=tvOS",
archiveName: "appletvos.xcarchive",
releaseFolder: "Release-appletvos",
buildSettings: nil
),
SDK(
destination: "generic/platform=tvOS Simulator",
archiveName: "appletvsimulator.xcarchive",
releaseFolder: "Release-appletvsimulator",
buildSettings: nil
)
]
case .watchos:
return [
SDK(
destination: "generic/platform=watchOS",
archiveName: "watchos.xcarchive",
releaseFolder: "Release-watchos",
buildSettings: nil
),
SDK(
destination: "generic/platform=watchOS Simulator",
archiveName: "watchsimulator.xcarchive",
releaseFolder: "Release-watchsimulator",
buildSettings: nil
)
]
}
}
}

View File

@@ -0,0 +1,9 @@
//
// main.swift
// Cargo
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
Command.main()