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,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)
})
}
}