diff --git a/Sources/Arguments.swift b/Sources/Arguments.swift index cf69c72a..7025c689 100644 --- a/Sources/Arguments.swift +++ b/Sources/Arguments.swift @@ -324,13 +324,15 @@ func parseCommaDelimitedList(_ string: String) -> [String] { /// 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) throws -> [String] { +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) { @@ -348,6 +350,18 @@ func parseRules(_ rules: String) throws -> [String] { } } +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) @@ -369,6 +383,7 @@ func parsePaths(_ paths: String, in directory: String) throws -> [URL] { /// 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 @@ -661,6 +676,7 @@ private func processOption(_ key: String, /// Parse rule names from arguments public func rulesFor(_ args: [String: String], lint: Bool, initial: Set? = nil) throws -> Set { + let parseRules = curryParseRules(config: args) var rules = initial ?? allRules if let specifiedRules = try args["rules"].map({ try Set(parseRules($0)) }) { @@ -779,6 +795,7 @@ func warningsForArguments(_ args: [String: String], ignoreUnusedOptions: Bool = 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)") @@ -837,6 +854,7 @@ let commandLineArguments = [ "strict", "verbose", "quiet", + "unknown-rules", "reporter", "report", // Misc diff --git a/Sources/CommandLine.swift b/Sources/CommandLine.swift index f63092ec..d53fc8cd 100644 --- a/Sources/CommandLine.swift +++ b/Sources/CommandLine.swift @@ -208,6 +208,7 @@ func printHelp(as type: CLI.OutputType) { --conflict-markers \(stripMarkdown(Descriptors.ignoreConflictMarkers.help)) --swift-version \(stripMarkdown(Descriptors.swiftVersion.help)) --language-mode \(stripMarkdown(Descriptors.languageMode.help)) + --unknown-rules How unknown rules are handled: "error" (default) or "ignore" --min-version The minimum SwiftFormat version to be used for these files --cache Path to cache file, or "clear" or "ignore" the default cache --dry-run Run in "dry" mode (without actually changing any files) @@ -492,7 +493,7 @@ func processArguments(_ args: [String], environment: [String: String] = [:], in } // Show rule info - if let names = try args["rule-info"].map(parseRules) { + if let names = try args["rule-info"].map({ try parseRules($0, ignoreUnknown: false) }) { let names = names.isEmpty ? allRules.sorted() : names.sorted() for name in names { try printRuleInfo(for: name, as: .content) diff --git a/Tests/ArgumentsTests.swift b/Tests/ArgumentsTests.swift index ee57dd7a..54e48d81 100644 --- a/Tests/ArgumentsTests.swift +++ b/Tests/ArgumentsTests.swift @@ -660,7 +660,7 @@ class ArgumentsTests: XCTestCase { let args = ["rules": "braces,fileHeader"] let config = ["rules": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let rules = try parseRules(result["rules"]!) + let rules = try parseRules(result["rules"]!, ignoreUnknown: false) XCTAssertEqual(rules, ["braces", "fileHeader"]) } @@ -668,7 +668,7 @@ class ArgumentsTests: XCTestCase { let args = ["rules": ""] let config = ["rules": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let rules = try parseRules(result["rules"]!) + let rules = try parseRules(result["rules"]!, ignoreUnknown: false) XCTAssertEqual(Set(rules), Set(["braces", "consecutiveSpaces"])) } @@ -676,7 +676,7 @@ class ArgumentsTests: XCTestCase { let args = ["enable": "braces,fileHeader"] let config = ["enable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let enabled = try parseRules(result["enable"]!) + let enabled = try parseRules(result["enable"]!, ignoreUnknown: false) XCTAssertEqual(enabled, ["braces", "consecutiveSpaces", "fileHeader"]) } @@ -684,7 +684,7 @@ class ArgumentsTests: XCTestCase { let args = ["disable": "braces,fileHeader"] let config = ["disable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let disabled = try parseRules(result["disable"]!) + let disabled = try parseRules(result["disable"]!, ignoreUnknown: false) XCTAssertEqual(disabled, ["braces", "consecutiveSpaces", "fileHeader"]) } @@ -692,7 +692,7 @@ class ArgumentsTests: XCTestCase { let args = ["rules": "braces,fileHeader"] let config = ["rules": "consecutiveSpaces", "disable": "braces", "enable": "redundantSelf"] let result = try mergeArguments(args, into: config) - let disabled = try parseRules(result["rules"]!) + let disabled = try parseRules(result["rules"]!, ignoreUnknown: false) XCTAssertEqual(disabled, ["braces", "fileHeader"]) XCTAssertNil(result["enabled"]) XCTAssertNil(result["disabled"]) @@ -702,11 +702,11 @@ class ArgumentsTests: XCTestCase { let args = ["enable": "braces"] let config = ["rules": "fileHeader", "disable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let rules = try parseRules(result["rules"]!) + let rules = try parseRules(result["rules"]!, ignoreUnknown: false) XCTAssertEqual(rules, ["fileHeader"]) - let enabled = try parseRules(result["enable"]!) + let enabled = try parseRules(result["enable"]!, ignoreUnknown: false) XCTAssertEqual(enabled, ["braces"]) - let disabled = try parseRules(result["disable"]!) + let disabled = try parseRules(result["disable"]!, ignoreUnknown: false) XCTAssertEqual(disabled, ["consecutiveSpaces"]) } @@ -714,11 +714,11 @@ class ArgumentsTests: XCTestCase { let args = ["disable": "braces"] let config = ["rules": "braces,fileHeader", "enable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) - let rules = try parseRules(result["rules"]!) + let rules = try parseRules(result["rules"]!, ignoreUnknown: false) XCTAssertEqual(rules, ["fileHeader"]) - let enabled = try parseRules(result["enable"]!) + let enabled = try parseRules(result["enable"]!, ignoreUnknown: false) XCTAssertEqual(enabled, ["consecutiveSpaces"]) - let disabled = try parseRules(result["disable"]!) + let disabled = try parseRules(result["disable"]!, ignoreUnknown: false) XCTAssertEqual(disabled, ["braces"]) } @@ -850,29 +850,39 @@ class ArgumentsTests: XCTestCase { // MARK: parse rules func testParseRulesCaseInsensitive() throws { - let rules = try parseRules("strongoutlets") + let rules = try parseRules("strongoutlets", ignoreUnknown: false) XCTAssertEqual(rules, ["strongOutlets"]) } func testParseAllRule() throws { - let rules = try parseRules("all") + let rules = try parseRules("all", ignoreUnknown: false) XCTAssertEqual(rules, FormatRules.all.compactMap { $0.isDeprecated ? nil : $0.name }) } func testParseInvalidRuleThrows() { - XCTAssertThrowsError(try parseRules("strongOutlet")) { error in + XCTAssertThrowsError(try parseRules("strongOutlet", ignoreUnknown: false)) { error in XCTAssertEqual("\(error)", "Unknown rule 'strongOutlet'. Did you mean 'strongOutlets'?") } } func testParseOptionAsRuleThrows() { - XCTAssertThrowsError(try parseRules("import-grouping")) { error in + XCTAssertThrowsError(try parseRules("import-grouping", ignoreUnknown: false)) { error in XCTAssert("\(error)".contains("'sortImports'")) } } + func testSuppressInvalidRuleError() throws { + let rules = try parseRules("strongOutlet,isEmpty", ignoreUnknown: true) + XCTAssertEqual(rules, ["isEmpty"]) + } + + func testSuppressOptionAsRuleError() throws { + let rules = try parseRules("import-grouping,isEmpty", ignoreUnknown: true) + XCTAssertEqual(rules, ["isEmpty"]) + } + // MARK: lintonly func testLintonlyRulesContain() throws {