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