Files
2025-10-16 07:16:56 +01:00

877 lines
32 KiB
Swift

//
// Arguments.swift
// SwiftFormat
//
// Created by Nick Lockwood on 07/08/2018.
// Copyright © 2018 Nick Lockwood.
//
// Distributed under the permissive MIT license
// Get the latest version from here:
//
// https://github.com/nicklockwood/SwiftFormat
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
extension Options {
init(_ args: [String: String], filterOptions: [Glob: [String: String]] = [:], in directory: String) throws {
fileOptions = try fileOptionsFor(args, in: directory)
formatOptions = try formatOptionsFor(args)
configURLs = args["config"].map {
parseCommaDelimitedList($0).map { expandPath($0, in: directory) }
}
let lint = args.keys.contains("lint")
self.lint = lint
rules = try rulesFor(args, lint: lint)
self.filterOptions = filterOptions
}
mutating func addArguments(_ args: [[String: String]], in directory: String) throws {
for args in args {
try addArguments(args, in: directory)
}
}
mutating func addArguments(_ args: [String: String], in directory: String) throws {
let oldArguments = argumentsFor(self)
let newArguments = try mergeArguments(args, into: oldArguments)
var newOptions = try Options(newArguments, filterOptions: filterOptions, in: directory)
if let fileInfo = formatOptions?.fileInfo {
newOptions.formatOptions?.fileInfo = fileInfo
}
newOptions.configURLs = configURLs
self = newOptions
}
/// Adds arguments from any `--filter`ed config that applies to this path
mutating func addFilterArguments(path: String) throws {
for (glob, options) in filterOptions {
if glob.matches(path) {
try applyArguments(options, lint: lint, to: &self)
}
}
}
}
extension Array where Element: Equatable {
func formattedList(default defaultValue: Element? = nil) -> String
where Element: RawRepresentable, Element.RawValue: Equatable
{
map(\.rawValue).formattedList(default: defaultValue?.rawValue)
}
func formattedList(default: Element? = nil) -> String {
if let `default` {
assert(contains(where: { $0 == `default` }))
}
let options: [String] = compactMap {
if "\($0)" == "default" {
return nil
} else if $0 == `default` {
return "\"\($0)\" (default)"
} else {
return "\"\($0)\""
}
}
return options.formattedList(lastSeparator: "or")
}
}
extension Array where Element: StringProtocol {
func formattedList(lastSeparator: String) -> String {
switch count {
case 0:
return ""
case 1:
return String(self[0])
case 2:
return "\(self[0]) \(lastSeparator) \(self[1])"
default:
return "\(dropLast().joined(separator: ", ")) \(lastSeparator) \(last!)"
}
}
}
extension String {
/// Find single best match for the string in a list of options
/// If more than one match is equally good, return nil
func bestMatch(in options: [String]) -> String? {
let matches = bestMatches(in: options)
guard let best = matches.first else {
return nil
}
if matches.count > 1, editDistance(from: matches[1]) == editDistance(from: best) {
return nil
}
return best
}
/// Find best matches for the string in a list of options
func bestMatches(in options: [String]) -> [String] {
let lowercaseQuery = lowercased()
// Sort matches by Levenshtein edit distance
return options
.compactMap { option -> (String, distance: Int, commonPrefix: Int)? in
let lowercaseOption = option.lowercased()
let distance = lowercaseOption.editDistance(from: lowercaseQuery)
let commonPrefix = lowercaseOption.commonPrefix(with: lowercaseQuery)
if commonPrefix.isEmpty, distance > lowercaseQuery.count / 2 {
return nil
}
return (option, distance, commonPrefix.count)
}
.sorted {
if $0.distance == $1.distance {
return $0.commonPrefix > $1.commonPrefix
}
return $0.distance < $1.distance
}
.map(\.0)
}
/// The Damerau-Levenshtein edit-distance between two strings
func editDistance(from other: String) -> Int {
let lhs = Array(self)
let rhs = Array(other)
var dist = [[Int]]()
for i in stride(from: 0, through: lhs.count, by: 1) {
dist.append([i])
}
for j in stride(from: 1, through: rhs.count, by: 1) {
dist[0].append(j)
}
for i in stride(from: 1, through: lhs.count, by: 1) {
for j in stride(from: 1, through: rhs.count, by: 1) {
if lhs[i - 1] == rhs[j - 1] {
dist[i].append(dist[i - 1][j - 1])
} else {
dist[i].append(Swift.min(dist[i - 1][j] + 1,
dist[i][j - 1] + 1,
dist[i - 1][j - 1] + 1))
}
if i > 1, j > 1, lhs[i - 1] == rhs[j - 2], lhs[i - 2] == rhs[j - 1] {
dist[i][j] = Swift.min(dist[i][j], dist[i - 2][j - 2] + 1)
}
}
}
return dist[lhs.count][rhs.count]
}
}
/// Extract content from a string, stopping at unquoted comment markers
/// Handles quoted strings and escaped characters properly
private func contentBeforeUnquotedComment(in string: String) -> String {
var result = ""
var inQuotes = false
var escaped = false
for char in string {
if escaped {
result.append(char)
escaped = false
} else if char == "\\" {
result.append(char)
escaped = true
} else if char == "\"" {
result.append(char)
inQuotes.toggle()
} else if char == "#", !inQuotes {
// Found unquoted comment marker, stop here
break
} else {
result.append(char)
}
}
return result
}
/// Parse a space-delimited string into an array of command-line arguments
/// Replicates the behavior implemented by the console when parsing input
func parseArguments(_ argumentString: String, ignoreComments: Bool = true) -> [String] {
// First handle comments using the shared logic if comments are not ignored
let inputString = ignoreComments ? argumentString : contentBeforeUnquotedComment(in: argumentString)
var arguments = [""] // Arguments always begin with script path
var characters = String.UnicodeScalarView.SubSequence(inputString.unicodeScalars)
var string = ""
var escaped = false
var quoted = false
while let char = characters.popFirst() {
switch char {
case "\\" where !escaped:
escaped = true
case "\"" where !escaped && !quoted:
quoted = true
case "\"" where !escaped && quoted:
quoted = false
fallthrough
case " " where !escaped && !quoted:
if !string.isEmpty {
arguments.append(string)
}
string.removeAll()
case "\"" where escaped:
escaped = false
string.append("\"")
case _ where escaped && quoted:
string.append("\\")
fallthrough
default:
escaped = false
string.append(Character(char))
}
}
if !string.isEmpty {
arguments.append(string)
}
return arguments
}
/// Parse a flat array of command-line arguments into a dictionary of flags and values
func preprocessArguments(_ args: [String], _ names: [String]) throws -> [String: String] {
var anonymousArgs = 0
var namedArgs: [String: String] = [:]
var name = ""
for arg in args {
if arg.hasPrefix("--") {
let key = String(arg.unicodeScalars.dropFirst(2)).lowercased()
if names.contains(key) {
name = key
}
// Support legacy `--alloneword` option names by finding
// any matching `--kebab-case` option name.
else if !key.contains("-") {
for kebabCaseName in names {
let nonKebabCaseName = kebabCaseName.replacingOccurrences(of: "-", with: "")
if nonKebabCaseName == key {
name = kebabCaseName
break
}
}
}
if name.isEmpty {
guard let match = key.bestMatches(in: names).first else {
throw FormatError.options("Unknown option --\(key)")
}
throw FormatError.options("Unknown option --\(key). Did you mean --\(match)?")
}
namedArgs[name] = namedArgs[name] ?? ""
continue
} else if arg.hasPrefix("-") {
// Short argument names
let flag = String(arg.unicodeScalars.dropFirst()).lowercased()
guard let match = names.first(where: { $0.hasPrefix(flag) }) else {
throw FormatError.options("Unknown flag -\(flag)")
}
name = match
namedArgs[name] = namedArgs[name] ?? ""
continue
}
if name == "" {
// Argument is anonymous
name = String(anonymousArgs)
anonymousArgs += 1
}
var arg = arg
let hasTrailingComma = arg.hasSuffix(",") && arg != ","
if hasTrailingComma {
arg = String(arg.dropLast())
}
if let existing = namedArgs[name], !existing.isEmpty,
// TODO: find a more general way to represent merge-able options
["exclude", "unexclude", "disable", "enable", "lint-only", "rules", "config"].contains(name) ||
Descriptors.all.contains(where: {
$0.argumentName == name && $0.isSetType
})
{
namedArgs[name] = existing + "," + arg
} else {
namedArgs[name] = arg
}
if !hasTrailingComma {
name = ""
}
}
return namedArgs
}
/// Parse a comma-delimited list of items
func parseCommaDelimitedList(_ string: String) -> [String] {
string.components(separatedBy: ",").compactMap {
let item = $0.trimmingCharacters(in: .whitespacesAndNewlines)
return item.isEmpty ? nil : item
}
}
/// Parse a comma-delimited string into an array of rules
let allRules = Set(FormatRules.all.map(\.name))
let defaultRules = Set(FormatRules.default.map(\.name))
func parseRules(_ rules: String, ignoreUnknown: Bool) throws -> [String] {
try parseCommaDelimitedList(rules).flatMap { proposedName -> [String] in
let lowercaseName = proposedName.lowercased()
if let name = allRules.first(where: { $0.lowercased() == lowercaseName }) {
return [name]
} else if lowercaseName == "all" {
return FormatRules.all.compactMap { $0.isDeprecated ? nil : $0.name }
} else if ignoreUnknown {
return []
}
if Descriptors.all.contains(where: { $0.argumentName == lowercaseName }) {
for rule in FormatRules.all where rule.options.contains(lowercaseName) {
throw FormatError.options(
"'\(proposedName)' is not a formatting rule. Did you mean '\(rule.name)'?"
)
}
throw FormatError.options("'\(proposedName)' is not a formatting rule")
}
let message = "Unknown rule '\(proposedName)'"
guard let match = proposedName.bestMatches(in: Array(allRules)).first else {
throw FormatError.options(message)
}
throw FormatError.options("\(message). Did you mean '\(match)'?")
}
}
func curryParseRules(config: [String: String]) -> (String) throws -> [String] {
{
try parseRules($0, ignoreUnknown: config["unknown-rules"].map {
switch $0 {
case "ignore": return true
case "error": return false
default: throw FormatError.options("Unknown value '\($0)' for --unknown-rules option")
}
} ?? false)
}
}
/// Parse single file path, disallowing globs or commas
func parsePath(_ path: String, for argument: String, in directory: String) throws -> URL {
let expandedPath = expandPath(path, in: directory)
if !FileManager.default.fileExists(atPath: expandedPath.path) {
if path.contains(",") {
throw FormatError.options("\(argument) argument does not support multiple paths")
}
if pathContainsGlobSyntax(path) {
throw FormatError.options("\(argument) path cannot contain wildcards")
}
}
return expandedPath
}
/// Parse one or more comma-delimited file paths, expanding globs as required
func parsePaths(_ paths: String, in directory: String) throws -> [URL] {
try matchGlobs(expandGlobs(paths, in: directory), in: directory)
}
/// Merge two dictionaries of arguments
func mergeArguments(_ args: [String: String], into config: [String: String]) throws -> [String: String] {
let parseRules = curryParseRules(config: config)
var input = config
var output = args
// Merge excluded urls
if let exclude = output["exclude"].map(parseCommaDelimitedList),
var excluded = input["exclude"].map({ Set(parseCommaDelimitedList($0)) })
{
excluded.formUnion(exclude)
output["exclude"] = Array(excluded).sorted().joined(separator: ",")
}
// Merge unexcluded urls
if let unexclude = output["unexclude"].map(parseCommaDelimitedList),
var unexcluded = input["unexclude"].map({ Set(parseCommaDelimitedList($0)) })
{
unexcluded.formUnion(unexclude)
output["unexclude"] = Array(unexcluded).sorted().joined(separator: ",")
}
// Merge rules
if let rules = try output["rules"].map(parseRules) {
if rules.isEmpty {
output["rules"] = nil
} else {
input["rules"] = nil
input["enable"] = nil
input["disable"] = nil
input["lint-only"] = nil
}
} else {
if let _disable = try output["disable"].map(parseRules) {
if let rules = try input["rules"].map(parseRules) {
input["rules"] = Set(rules).subtracting(_disable).sorted().joined(separator: ",")
}
if let enable = try input["enable"].map(parseRules) {
input["enable"] = Set(enable).subtracting(_disable).sorted().joined(separator: ",")
}
if let lintonly = try input["lint-only"].map(parseRules) {
input["lint-only"] = Set(lintonly).subtracting(_disable).sorted().joined(separator: ",")
}
if let disable = try input["disable"].map(parseRules) {
input["disable"] = Set(disable).union(_disable).sorted().joined(separator: ",")
output["disable"] = nil
}
}
if let _enable = try output["enable"].map(parseRules) {
if let enable = try input["enable"].map(parseRules) {
input["enable"] = Set(enable).union(_enable).sorted().joined(separator: ",")
output["enable"] = nil
}
if let lintonly = try input["lint-only"].map(parseRules) {
input["lint-only"] = Set(lintonly).subtracting(_enable).sorted().joined(separator: ",")
}
if let disable = try input["disable"].map(parseRules) {
input["disable"] = Set(disable).subtracting(_enable).sorted().joined(separator: ",")
}
}
if let _lintonly = try output["lint-only"].map(parseRules) {
if let lintonly = try input["lint-only"].map(parseRules) {
input["lint-only"] = Set(lintonly).union(_lintonly).sorted().joined(separator: ",")
output["lint-only"] = nil
}
}
}
// Merge other arguments
for (key, inValue) in input where output[key] == nil {
output[key] = inValue
}
return output
}
/// Parse a configuration file into a list of argument dictionaries.
///
/// Config files can have headers that separate them into separate sections.
/// Each section is treated as if it were its own file. For example:
/// ```
/// --indent 4
///
/// [Tests]
/// --filter **/Tests/**
/// --indent 2
/// ```
public func parseConfigFile(_ data: Data) throws -> ([[String: String]]) {
guard let input = String(data: data, encoding: .utf8) else {
throw FormatError.reading("Unable to read data for configuration file")
}
let lines = try cumulate(successiveLines: input.components(separatedBy: .newlines))
let configSegments = lines.split(whereSeparator: { line in
line.starts(with: "[") && line.contains("]")
})
guard !configSegments.isEmpty else {
return []
}
var configOptions = [[String: String]]()
for configSegmentLines in configSegments {
let arguments = try configSegmentLines.flatMap { line -> [String] in
// TODO: parseArguments isn't a perfect fit here - should we use a different approach?
let line = line.replacingOccurrences(of: "\\n", with: "\n")
let parts = parseArguments(line, ignoreComments: false).dropFirst().map {
$0.replacingOccurrences(of: "\n", with: "\\n")
}
guard let key = parts.first else {
return []
}
if !key.hasPrefix("-") {
throw FormatError.options("Unknown option '\(key)' in configuration file")
}
return [key, parts.dropFirst().joined(separator: " ")]
}
do {
try configOptions.append(preprocessArguments(arguments, optionsArguments))
} catch let FormatError.options(message) {
throw FormatError.options("\(message) in configuration file")
}
}
return configOptions
}
private func cumulate(successiveLines: [String]) throws -> [String] {
var cumulatedLines = [String]()
var iterator = successiveLines.makeIterator()
while let currentLine = iterator.next() {
var cumulatedLine = contentBeforeUnquotedComment(in: currentLine).trimmingCharacters(in: .whitespaces)
while cumulatedLine.hasSuffix("\\") {
guard let nextLine = iterator.next() else {
throw FormatError.reading("Configuration file ends with an illegal line continuation character '\'")
}
if !nextLine.trimmingCharacters(in: .whitespaces).starts(with: "#") {
cumulatedLine = cumulatedLine.dropLast() + contentBeforeUnquotedComment(in: nextLine).trimmingCharacters(in: .whitespaces)
}
}
cumulatedLines.append(String(cumulatedLine))
}
return cumulatedLines
}
/// Serialize a set of options into either an arguments string or a file
public func serialize(options: Options,
swiftVersion: Version = .undefined,
excludingDefaults: Bool = false,
separator: String = "\n") -> String
{
var arguments = [[String: String]]()
if let fileOptions = options.fileOptions {
arguments.append(argumentsFor(
Options(fileOptions: fileOptions),
excludingDefaults: excludingDefaults
))
}
if let formatOptions = options.formatOptions {
arguments.append(argumentsFor(
Options(formatOptions: formatOptions),
excludingDefaults: excludingDefaults
))
} else if swiftVersion != .undefined {
let descriptor = Descriptors.swiftVersion
arguments.append([descriptor.argumentName: swiftVersion.rawValue])
}
if let rules = options.rules {
arguments.append(argumentsFor(
Options(rules: rules),
excludingDefaults: excludingDefaults
))
}
return arguments
.map { serialize(arguments: $0, separator: separator) }
.filter { !$0.isEmpty }
.joined(separator: separator)
}
/// Serialize arguments
func serialize(arguments: [String: String],
separator: String = "\n") -> String
{
arguments.map {
var value = $1
if value.contains(" ") {
value = "\"\(value.replacingOccurrences(of: "\"", with: "\\\""))\""
}
if value.contains("#") {
value = "\"\(value)\""
}
return "--\($0) \(value)"
}.sorted().joined(separator: separator)
}
/// Get command line arguments from options
func argumentsFor(_ options: Options, excludingDefaults: Bool = false) -> [String: String] {
var args = [String: String]()
if let fileOptions = options.fileOptions {
var arguments = Set(fileArguments)
do {
if !excludingDefaults || fileOptions.followSymlinks != FileOptions.default.followSymlinks {
args["symlinks"] = fileOptions.followSymlinks ? "follow" : "ignore"
}
arguments.remove("symlinks")
}
do {
if !fileOptions.excludedGlobs.isEmpty {
// TODO: find a better alternative to stringifying url
args["exclude"] = fileOptions.excludedGlobs.map(\.description).sorted().joined(separator: ",")
}
arguments.remove("exclude")
}
do {
if !fileOptions.unexcludedGlobs.isEmpty {
// TODO: find a better alternative to stringifying url
args["unexclude"] = fileOptions.unexcludedGlobs.map(\.description).sorted().joined(separator: ",")
}
arguments.remove("unexclude")
}
do {
if !excludingDefaults || fileOptions.minVersion != FileOptions.default.minVersion {
args["min-version"] = fileOptions.minVersion.description
}
arguments.remove("min-version")
}
arguments.remove("filter")
assert(arguments.isEmpty)
}
if let formatOptions = options.formatOptions {
for descriptor in Descriptors.all where !descriptor.isRenamed && !descriptor.isDeprecated {
let value = descriptor.fromOptions(formatOptions)
guard value != descriptor.fromOptions(.default) ||
(!excludingDefaults && !descriptor.isDeprecated)
else {
continue
}
// Special case for swiftVersion and languageMode
// TODO: find a better solution for this
if descriptor.argumentName == Descriptors.swiftVersion.argumentName,
value == Version.undefined.rawValue
{
continue
}
if descriptor.argumentName == Descriptors.languageMode.argumentName,
value == defaultLanguageMode(for: formatOptions.swiftVersion).rawValue
{
continue
}
args[descriptor.argumentName] = value
}
}
if options.lint {
args["lint"] = ""
}
if let rules = options.rules {
let enabled = rules.subtracting(defaultRules)
if !enabled.isEmpty {
args["enable"] = enabled.sorted().joined(separator: ",")
}
let disabled = defaultRules.subtracting(rules)
if !disabled.isEmpty {
args["disable"] = disabled.sorted().joined(separator: ",")
}
}
return args
}
private func processOption(_ key: String,
in args: [String: String],
from: inout Set<String>,
handler: (String) throws -> Void) throws
{
precondition(optionsArguments.contains(key), "\(key) not in optionsArguments")
var arguments = from
arguments.remove(key)
from = arguments
guard let value = args[key] else {
return
}
do {
try handler(value)
} catch {
guard !value.isEmpty else {
throw FormatError.options("--\(key) option expects a value")
}
if case var FormatError.options(string) = error, !string.isEmpty {
if !string.contains(key) {
string += " in --\(key)"
}
throw FormatError.options(string)
}
throw FormatError.options("Unsupported --\(key) value '\(value)'")
}
}
/// Parse rule names from arguments
public func rulesFor(_ args: [String: String], lint: Bool, initial: Set<String>? = nil) throws -> Set<String> {
let parseRules = curryParseRules(config: args)
var rules = initial ?? allRules
if let specifiedRules = try args["rules"].map({ try Set(parseRules($0)) }) {
rules = specifiedRules
} else if initial == nil {
rules = rules.subtracting(FormatRules.disabledByDefault.map(\.name))
}
try args["disable"].map {
try rules.subtract(parseRules($0))
}
try args["enable"].map {
try rules.formUnion(parseRules($0))
}
try args["lint-only"].map { rulesString in
if lint {
try rules.formUnion(parseRules(rulesString))
} else {
try rules.subtract(parseRules(rulesString))
}
}
return rules
}
/// Parse FileOptions from arguments
func fileOptionsFor(_ args: [String: String], in directory: String) throws -> FileOptions? {
var options = FileOptions()
var arguments = Set(fileArguments)
var containsFileOption = false
try processOption("symlinks", in: args, from: &arguments) {
containsFileOption = true
switch $0.lowercased() {
case "follow":
options.followSymlinks = true
case "ignore":
options.followSymlinks = false
default:
throw FormatError.options("")
}
}
try processOption("exclude", in: args, from: &arguments) {
containsFileOption = true
options.excludedGlobs += expandGlobs($0, in: directory)
}
try processOption("unexclude", in: args, from: &arguments) {
containsFileOption = true
options.unexcludedGlobs += expandGlobs($0, in: directory)
}
try processOption("min-version", in: args, from: &arguments) {
containsFileOption = true
guard let minVersion = Version(rawValue: $0) else {
throw FormatError.options("Unsupported --min-version value '\($0)'")
}
guard minVersion <= Version(stringLiteral: swiftFormatVersion) else {
throw FormatError.options("Project specifies SwiftFormat --min-version of \(minVersion)")
}
options.minVersion = minVersion
}
try processOption("filter", in: args, from: &arguments, handler: { _ in
// no-op, handled in `Options.init` and `addArguments`
})
assert(arguments.isEmpty, "\(arguments.joined(separator: ","))")
return containsFileOption ? options : nil
}
/// Parse FormatOptions from arguments
/// Returns nil if the arguments dictionary does not contain any formatting arguments
public func formatOptionsFor(_ args: [String: String]) throws -> FormatOptions? {
var options = FormatOptions.default
let containsFormatOption = try applyFormatOptions(from: args, to: &options)
return containsFormatOption ? options : nil
}
public func applyFormatOptions(from args: [String: String], to formatOptions: inout FormatOptions) throws -> Bool {
var arguments = Set(formattingArguments)
var containsFormatOption = false
for option in Descriptors.all {
try processOption(option.argumentName, in: args, from: &arguments) {
containsFormatOption = true
do {
try option.toOptions($0, &formatOptions)
} catch {
guard let names = option.validArguments else {
throw error
}
throw FormatError.invalidOption($0, for: option.argumentName, with: names)
}
}
}
assert(arguments.isEmpty, "\(arguments.joined(separator: ","))")
return containsFormatOption
}
/// Applies additional arguments to the given `Options` struct
func applyArguments(_ args: [String: String], lint: Bool, to options: inout Options) throws {
options.rules = try rulesFor(args, lint: lint, initial: options.rules)
var formatOptions = options.formatOptions ?? .default
let containsFormatOption = try applyFormatOptions(from: args, to: &formatOptions)
if containsFormatOption {
options.formatOptions = formatOptions
}
}
/// Get deprecation warnings from a set of arguments
func warningsForArguments(_ args: [String: String], ignoreUnusedOptions: Bool = false) -> [String] {
var warnings = [String]()
for option in Descriptors.all {
if args[option.argumentName] != nil, let message = option.deprecationMessage {
warnings.append("--\(option.argumentName) option is deprecated. \(message)")
}
}
let parseRules = curryParseRules(config: args)
for name in Set(rulesArguments.flatMap { (try? args[$0].map(parseRules) ?? []) ?? [] }) {
if let message = FormatRules.byName[name]?.deprecationMessage {
warnings.append("\(name) rule is deprecated. \(message)")
}
}
if !ignoreUnusedOptions, let rules = try? rulesFor(args, lint: true) {
for arg in args.keys where formattingArguments.contains(arg) {
if !rules.contains(where: {
guard let rule = FormatRules.byName[$0] else {
return false
}
return rule.options.contains(arg) || rule.sharedOptions.contains(arg)
}), let expected = FormatRules.all.first(where: {
$0.options.contains(arg)
})?.name {
warnings.append("--\(arg) option has no effect when \(expected) rule is disabled")
}
}
}
return warnings
}
let fileArguments = [
"symlinks",
"exclude",
"unexclude",
"min-version",
"filter",
]
let rulesArguments = [
"disable",
"enable",
"lint-only",
"rules",
]
let formattingArguments = Descriptors.formatting.map(\.argumentName)
let internalArguments = Descriptors.internal.map(\.argumentName)
let optionsArguments = fileArguments + rulesArguments + formattingArguments + internalArguments
let commandLineArguments = [
// Input options
"filelist",
"stdin-path",
"script-input",
"config",
"base-config",
"infer-options",
"line-range",
"output",
"cache",
"dry-run",
"lint",
"lenient",
"strict",
"verbose",
"quiet",
"unknown-rules",
"reporter",
"report",
// Misc
"help",
"version",
"options",
"rule-info",
"date-format",
"timezone",
"output-tokens",
] + optionsArguments
let deprecatedArguments = Descriptors.deprecated.map(\.argumentName)