// // ArgumentsTests.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 XCTest @testable import SwiftFormat final class ArgumentsTests: XCTestCase { // MARK: arg parser func testParseSimpleArguments() { let input = "hello world" let output = ["", "hello", "world"] XCTAssertEqual(parseArguments(input), output) } func testParseEscapedSpace() { let input = "hello\\ world" let output = ["", "hello world"] XCTAssertEqual(parseArguments(input), output) } func testParseEscapedN() { let input = "hello\\nworld" let output = ["", "hellonworld"] XCTAssertEqual(parseArguments(input), output) } func testParseQuoteArguments() { let input = "\"hello world\"" let output = ["", "hello world"] XCTAssertEqual(parseArguments(input), output) } func testParseEscapedQuote() { let input = "hello \\\"world\\\"" let output = ["", "hello", "\"world\""] XCTAssertEqual(parseArguments(input), output) } func testParseEscapedQuoteInString() { let input = "\"hello \\\"world\\\"\"" let output = ["", "hello \"world\""] XCTAssertEqual(parseArguments(input), output) } func testParseQuotedEscapedN() { let input = "\"hello\\nworld\"" let output = ["", "hello\\nworld"] XCTAssertEqual(parseArguments(input), output) } func testCommentedLine() { let input = "#hello" let output = [""] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testCommentInLine() { let input = "hello#world" let output = ["", "hello"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testCommentAfterSpace() { let input = "hello #world" let output = ["", "hello"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testCommentBeforeSpace() { let input = "hello# world" let output = ["", "hello"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testCommentContainingSpace() { let input = "hello #wide world" let output = ["", "hello"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testEscapedComment() { let input = "hello \\#world" let output = ["", "hello", "#world"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testQuotedComment() { let input = "hello \"#world\"" let output = ["", "hello", "#world"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testQuotedURLMacro() { let input = "--url-macro \"#URL,URLFoundation\"" let output = ["", "--url-macro", "#URL,URLFoundation"] XCTAssertEqual(parseArguments(input, ignoreComments: false), output) } func testLegacyOptionsWithLegacyOptionNames() throws { let testCases: [(legacy: String, current: String)] = [ ("lineaftermarks", "line-after-marks"), ("indentcase", "indent-case"), ("trailingcommas", "trailing-commas"), ("wrapArguments", "wrap-arguments"), ("hexliteralcase", "hex-literal-case"), ("nospaceoperators", "no-space-operators"), ("modifierorder", "modifier-order"), ("extensionACL", "extension-acl"), ("propertyTypes", "property-types"), ("swiftVersion", "swift-version"), ] for (legacy, current) in testCases { do { let legacyArgs = try preprocessArguments(["", "--\(legacy)", "true"], commandLineArguments) let currentArgs = try preprocessArguments(["", "--\(current)", "true"], commandLineArguments) // Both should map to the same internal option name XCTAssertEqual(legacyArgs[current] ?? legacyArgs[legacy], currentArgs[current]) } catch { XCTFail("Legacy option --\(legacy) should work but failed with: \(error)") } } } // MARK: arg preprocessor func testPreprocessArguments() { let input = ["", "foo", "bar", "-o", "baz", "-i", "4", "-l", "cr", "-s", "inline"] let output = ["0": "", "1": "foo", "2": "bar", "output": "baz", "indent": "4", "linebreaks": "cr", "semicolons": "inline"] XCTAssertEqual(try preprocessArguments(input, [ "output", "indent", "linebreaks", "semicolons", ]), output) } func testPreprocessArgumentsNormalizesKeyCase() { let input = ["", "--Help", "-VERSION", "-foo", "BaR"] let output = ["0": "", "help": "", "version": "", "foo": "BaR"] XCTAssertEqual(try preprocessArguments(input, [ "help", "version", "foo", ]), output) } func testEmptyArgsAreRecognized() { let input = ["", "--help", "--version"] let output = ["0": "", "help": "", "version": ""] XCTAssertEqual(try preprocessArguments(input, [ "help", "version", ]), output) } func testInvalidArgumentThrows() { XCTAssertThrowsError(try preprocessArguments(["", "--vers"], [ "verbose", "version", ])) { error in XCTAssertEqual("\(error)", "Unknown option --vers. Did you mean --version?") } } // merging func testDuplicateDisableArgumentsAreMerged() { let input = ["", "--disable", "foo", "--disable", "bar"] let output = ["0": "", "disable": "foo,bar"] XCTAssertEqual(try preprocessArguments(input, [ "disable", ]), output) } func testDuplicateExcludeArgumentsAreMerged() { let input = ["", "--exclude", "foo", "--exclude", "bar"] let output = ["0": "", "exclude": "foo,bar"] XCTAssertEqual(try preprocessArguments(input, [ "exclude", ]), output) } func testDuplicateUnexcludeArgumentsAreMerged() { let input = ["", "--unexclude", "foo", "--unexclude", "bar"] let output = ["0": "", "unexclude": "foo,bar"] XCTAssertEqual(try preprocessArguments(input, [ "unexclude", ]), output) } func testDuplicateSelfrequiredArgumentsAreMerged() { let input = ["", "--self-required", "foo", "--self-required", "bar"] let output = ["0": "", "self-required": "foo,bar"] XCTAssertEqual(try preprocessArguments(input, [ "self-required", ]), output) } func testDuplicateNoSpaceOperatorsArgumentsAreMerged() { let input = ["", "--nospaceoperators", "+", "--nospaceoperators", "*"] let output = ["0": "", "no-space-operators": "+,*"] XCTAssertEqual(try preprocessArguments(input, [ "no-space-operators", ]), output) } func testDuplicateNoWrapOperatorsArgumentsAreMerged() { let input = ["", "--nowrapoperators", "+", "--nowrapoperators", "."] let output = ["0": "", "no-wrap-operators": "+,."] XCTAssertEqual(try preprocessArguments(input, [ "no-wrap-operators", ]), output) } func testDuplicateRangesArgumentsAreNotMerged() { let input = ["", "--ranges", "spaced", "--ranges", "no-space"] let output = ["0": "", "ranges": "no-space"] XCTAssertEqual(try preprocessArguments(input, [ "ranges", ]), output) } // comma-delimited values func testSpacesIgnoredInCommaDelimitedArguments() { let input = ["", "--rules", "foo,", "bar"] let output = ["0": "", "rules": "foo,bar"] XCTAssertEqual(try preprocessArguments(input, [ "rules", ]), output) } func testNextArgumentNotIgnoredAfterCommaInArguments() { let input = ["", "--enable", "foo,", "--disable", "bar"] let output = ["0": "", "enable": "foo", "disable": "bar"] XCTAssertEqual(try preprocessArguments(input, [ "enable", "disable", ]), output) } // flags func testVMatchesVerbose() { let input = ["", "-v"] let output = ["0": "", "verbose": ""] XCTAssertEqual(try preprocessArguments(input, commandLineArguments), output) } func testHMatchesHelp() { let input = ["", "-h"] let output = ["0": "", "help": ""] XCTAssertEqual(try preprocessArguments(input, commandLineArguments), output) } func testOMatchesOutput() { let input = ["", "-o"] let output = ["0": "", "output": ""] XCTAssertEqual(try preprocessArguments(input, commandLineArguments), output) } func testNoMatchFlagThrows() { let input = ["", "-v"] XCTAssertThrowsError(try preprocessArguments(input, [ "help", "file", ])) } // MARK: format options to arguments func testCommandLineArgumentsHaveValidNames() { for key in argumentsFor(.default).keys { XCTAssertTrue(optionsArguments.contains(key), "\(key) is not a valid argument name") } } func testFormattingArgumentsAreAllImplemented() throws { CLI.print = { _, _ in } for key in formattingArguments { guard let value = argumentsFor(.default)[key] else { XCTAssert(deprecatedArguments.contains(key)) continue } XCTAssert(!deprecatedArguments.contains(key), key) _ = try formatOptionsFor([key: value]) } } func testEmptyFormatOptions() throws { XCTAssertNil(try formatOptionsFor([:])) XCTAssertNil(try formatOptionsFor(["--disable": "void"])) } func testFileHeaderOptionToArguments() { let options = FormatOptions(fileHeader: "// Hello World\n// Goodbye World") let args = argumentsFor(Options(formatOptions: options), excludingDefaults: true) XCTAssertEqual(args["header"], "// Hello World\\n// Goodbye World") } // TODO: should this go in OptionDescriptorTests instead? func testRenamedArgument() { XCTAssert(Descriptors.specifierOrder.isRenamed) } // MARK: config file parsing func testParseArgumentsContainingBlankLines() { let config = """ --allman true --rules braces,fileHeader """ let data = Data(config.utf8) do { let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 2) } catch { XCTFail("\(error)") } } func testParseConfigFileWithHeaders() { let config = """ --allman true --rules braces,fileHeader [Header] --filter Foo --allman false [Header2] --filter Bar --indent 4 """ let data = Data(config.utf8) do { let segments = try parseConfigFile(data) XCTAssertEqual(segments.count, 3) XCTAssertEqual(segments[0].count, 2) XCTAssertEqual(segments[1].count, 2) XCTAssertEqual(segments[2].count, 2) } catch { XCTFail("\(error)") } } func testParseArgumentsContainingAnonymousValues() throws { let config = """ hello --allman true """ let data = Data(config.utf8) XCTAssertThrowsError(try parseConfigFile(data)[0]) { error in guard case let FormatError.options(message) = error else { XCTFail("\(error)") return } XCTAssert(message.contains("hello")) } } func testParseArgumentsContainingSpaces() throws { let config = "--rules braces, fileHeader, consecutiveSpaces" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["rules"], "braces, fileHeader, consecutiveSpaces") } func testParseURLMacroArgumentInConfigFileFailsWithoutQuotes() throws { let config = "--url-macro #URL,URLFoundation" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] // This should fail because #URL,URLFoundation is treated as a comment XCTAssertEqual(args.count, 1) XCTAssertEqual(args["url-macro"], "") } func testParseURLMacroArgumentInConfigFileWithQuotes() throws { let config = "--url-macro \"#URL,URLFoundation\"" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["url-macro"], "#URL,URLFoundation") } func testParseArgumentsOnMultipleLines() throws { let config = """ --rules braces, \\ fileHeader, \\ andOperator, typeSugar --allman true --hexgrouping \\ 4, \\ 8 """ let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args["rules"], "braces, fileHeader, andOperator, typeSugar") XCTAssertEqual(args["allman"], "true") XCTAssertEqual(args["hex-grouping"], "4, 8") } func testCommentsInConsecutiveLines() throws { let config = """ --rules braces, \\ # some comment fileHeader, \\ # another comment invalidating this line separator \\ # yet another comment andOperator --hexgrouping \\ 4, \\ # comment after line separator 8 # comment invalidating this line separator \\ """ let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args["rules"], "braces, fileHeader, andOperator") XCTAssertEqual(args["hex-grouping"], "4, 8") } func testLineContinuationCharacterOnLastLine() throws { let config = """ --rules braces,\\ fileHeader\\ """ let data = Data(config.utf8) XCTAssertThrowsError(try parseConfigFile(data)[0]) { XCTAssert($0.localizedDescription.contains("line continuation character")) } } func testParseArgumentsContainingEscapedCharacters() throws { let config = "--header hello\\ world\\ngoodbye\\ world" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["header"], "hello world\\ngoodbye world") } func testParseArgumentsContainingQuotedCharacters() throws { let config = """ --header "hello world\\ngoodbye world" """ let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["header"], "hello world\\ngoodbye world") } func testParseIgnoreFileHeader() throws { let config = "--header ignore" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] let options = try Options(args, in: "/") XCTAssertEqual(options.formatOptions?.fileHeader, .ignore) } func testParseUppercaseIgnoreFileHeader() throws { let config = "--header IGNORE" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] let options = try Options(args, in: "/") XCTAssertEqual(options.formatOptions?.fileHeader, .ignore) } func testParseArgumentsContainingSwiftVersion() throws { let config = "--swiftversion 5.1" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["swift-version"], "5.1") } func testParseArgumentsContainingLanguageVersion() throws { let config = "--languagemode 6" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] XCTAssertEqual(args.count, 1) XCTAssertEqual(args["language-mode"], "6") } func testParseArgumentsContainingDisableAll() throws { let config = "--disable all" let data = Data(config.utf8) let args = try parseConfigFile(data)[0] let options = try Options(args, in: "/") XCTAssertEqual(options.rules, []) } func testPopulatesDefaultLanguageMode() { let swift5Options = FormatOptions(swiftVersion: "5.0") XCTAssertEqual(swift5Options.languageMode, "5") let swift6Options = FormatOptions(swiftVersion: "6.0") XCTAssertEqual(swift6Options.languageMode, "5") let swift6LangModeOptions = FormatOptions(swiftVersion: "6.0", languageMode: "6") XCTAssertEqual(swift6LangModeOptions.languageMode, "6") } // MARK: config file serialization // file header comment encoding func testSerializeFileHeaderContainingSpace() { let options = Options(formatOptions: FormatOptions(fileHeader: "// hello world")) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, "--header \"// hello world\"") } func testSerializeFileHeaderContainingEscapedSpace() { let options = Options(formatOptions: FormatOptions(fileHeader: "// hello\\ world")) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, "--header \"// hello\\ world\"") } func testSerializeFileHeaderContainingLinebreak() { let options = Options(formatOptions: FormatOptions(fileHeader: "//hello\nworld")) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, "--header //hello\\nworld") } func testSerializeFileHeaderContainingLinebreakAndSpaces() { let options = Options(formatOptions: FormatOptions(fileHeader: "// hello\n// world")) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, "--header \"// hello\\n// world\"") } func testSerializeOptionsWithPoundCharacter() { let options = Options(formatOptions: FormatOptions( urlMacro: .macro("#URL", module: "URLFoundation"), preferFileMacro: false )) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, """ --file-macro "#fileID" --url-macro "#URL,URLFoundation" """) } // trailing separator func testSerializeOptionsDisabledDefaultRulesEnabledIsEmpty() { let rules = defaultRules let config: String = serialize(options: Options(formatOptions: nil, rules: rules)) XCTAssertEqual(config, "") } func testSerializeOptionsDisabledAllRulesEnabledNoTerminatingSeparator() { let rules = allRules let config: String = serialize(options: Options(formatOptions: nil, rules: rules)) XCTAssertFalse(config.contains("--disable")) XCTAssertNotEqual(config.last, "\n") } func testSerializeOptionsDisabledSomeRulesDisabledNoTerminatingSeparator() { let rules = Set(defaultRules.prefix(3)) let config: String = serialize(options: Options(formatOptions: nil, rules: rules)) XCTAssertTrue(config.contains("--disable")) XCTAssertFalse(config.contains("--enable")) XCTAssertNotEqual(config.last, "\n") } func testSerializeOptionsEnabledDefaultRulesEnabledNoTerminatingSeparator() { let rules = defaultRules let config: String = serialize(options: Options(formatOptions: .default, rules: rules)) XCTAssertNotEqual(config, "") XCTAssertFalse(config.contains("--disable")) XCTAssertFalse(config.contains("--enable")) XCTAssertNotEqual(config.last, "\n") } func testSerializeOptionsEnabledAllRulesEnabledNoTerminatingSeparator() { let rules = allRules let config: String = serialize(options: Options(formatOptions: .default, rules: rules)) XCTAssertFalse(config.contains("--disable")) XCTAssertNotEqual(config.last, "\n") } func testSerializeOptionsEnabledSomeRulesDisabledNoTerminatingSeparator() { let rules = Set(defaultRules.prefix(3)) let config: String = serialize(options: Options(formatOptions: .default, rules: rules)) XCTAssertTrue(config.contains("--disable")) XCTAssertFalse(config.contains("--enable")) XCTAssertNotEqual(config.last, "\n") } // swift version func testSerializeSwiftVersion() { let version = Version(rawValue: "5.2") ?? "0" let options = Options(formatOptions: FormatOptions(swiftVersion: version)) let config = serialize(options: options, excludingDefaults: true) XCTAssertEqual(config, "--swift-version 5.2") } // MARK: config file merging func testMergeFormatOptionArguments() throws { let args = ["allman": "false", "commas": "always"] let config = ["allman": "true", "binary-grouping": "4,8"] let result = try mergeArguments(args, into: config) for (key, value) in result { // args take precedence over config XCTAssertEqual(value, args[key] ?? config[key]) } for key in Set(args.keys).union(config.keys) { // all keys should be present in result XCTAssertNotNil(result[key]) } } func testMergeExcludedURLs() throws { let args = ["exclude": "foo,bar"] let config = ["exclude": "bar,baz"] let result = try mergeArguments(args, into: config) XCTAssertEqual(result["exclude"], "bar,baz,foo") } func testMergeUnexcludedURLs() throws { let args = ["unexclude": "foo,bar"] let config = ["unexclude": "bar,baz"] let result = try mergeArguments(args, into: config) XCTAssertEqual(result["unexclude"], "bar,baz,foo") } func testMergeRules() throws { let args = ["rules": "braces,fileHeader"] let config = ["rules": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let rules = try parseRules(XCTUnwrap(result["rules"]), ignoreUnknown: false) XCTAssertEqual(rules, ["braces", "fileHeader"]) } func testMergeEmptyRules() throws { let args = ["rules": ""] let config = ["rules": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let rules = try parseRules(XCTUnwrap(result["rules"]), ignoreUnknown: false) XCTAssertEqual(Set(rules), Set(["braces", "consecutiveSpaces"])) } func testMergeEnableRules() throws { let args = ["enable": "braces,fileHeader"] let config = ["enable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let enabled = try parseRules(XCTUnwrap(result["enable"]), ignoreUnknown: false) XCTAssertEqual(enabled, ["braces", "consecutiveSpaces", "fileHeader"]) } func testMergeDisableRules() throws { let args = ["disable": "braces,fileHeader"] let config = ["disable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let disabled = try parseRules(XCTUnwrap(result["disable"]), ignoreUnknown: false) XCTAssertEqual(disabled, ["braces", "consecutiveSpaces", "fileHeader"]) } func testRulesArgumentOverridesAllConfigRules() throws { let args = ["rules": "braces,fileHeader"] let config = ["rules": "consecutiveSpaces", "disable": "braces", "enable": "redundantSelf"] let result = try mergeArguments(args, into: config) let disabled = try parseRules(XCTUnwrap(result["rules"]), ignoreUnknown: false) XCTAssertEqual(disabled, ["braces", "fileHeader"]) XCTAssertNil(result["enabled"]) XCTAssertNil(result["disabled"]) } func testEnabledArgumentOverridesConfigRules() throws { let args = ["enable": "braces"] let config = ["rules": "fileHeader", "disable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let rules = try parseRules(XCTUnwrap(result["rules"]), ignoreUnknown: false) XCTAssertEqual(rules, ["fileHeader"]) let enabled = try parseRules(XCTUnwrap(result["enable"]), ignoreUnknown: false) XCTAssertEqual(enabled, ["braces"]) let disabled = try parseRules(XCTUnwrap(result["disable"]), ignoreUnknown: false) XCTAssertEqual(disabled, ["consecutiveSpaces"]) } func testDisableArgumentOverridesConfigRules() throws { let args = ["disable": "braces"] let config = ["rules": "braces,fileHeader", "enable": "consecutiveSpaces,braces"] let result = try mergeArguments(args, into: config) let rules = try parseRules(XCTUnwrap(result["rules"]), ignoreUnknown: false) XCTAssertEqual(rules, ["fileHeader"]) let enabled = try parseRules(XCTUnwrap(result["enable"]), ignoreUnknown: false) XCTAssertEqual(enabled, ["consecutiveSpaces"]) let disabled = try parseRules(XCTUnwrap(result["disable"]), ignoreUnknown: false) XCTAssertEqual(disabled, ["braces"]) } func testMergeSelfRequiredOptions() throws { let args = ["self-required": "log,assert"] let config = ["self-required": "expect"] let result = try mergeArguments(args, into: config) let selfRequired = try parseCommaDelimitedList(XCTUnwrap(result["self-required"])) XCTAssertEqual(selfRequired, ["log", "assert"]) } func testMergeAcronyms() throws { let args = ["acronyms": "url"] let config = ["acronyms": "id,uuid"] let result = try mergeArguments(args, into: config) let acronyms = try parseCommaDelimitedList(XCTUnwrap(result["acronyms"])) XCTAssertEqual(acronyms, ["url"]) } // MARK: add arguments func testAddFormatArguments() throws { var options = Options( formatOptions: FormatOptions(indent: " ", semicolons: .inlineOnly) ) try options.addArguments(["indent": "2", "linebreaks": "crlf"], in: "") guard let formatOptions = options.formatOptions else { XCTFail() return } XCTAssertEqual(formatOptions.indent, " ") XCTAssertEqual(formatOptions.linebreak, "\r\n") XCTAssertEqual(formatOptions.semicolons, .inlineOnly) } func testAddArgumentsDoesntBreakSwiftVersion() throws { var options = Options(formatOptions: FormatOptions(swiftVersion: "4.2")) try options.addArguments(["indent": "2"], in: "") guard let formatOptions = options.formatOptions else { XCTFail() return } XCTAssertEqual(formatOptions.swiftVersion, "4.2") } func testAddArgumentsDoesntBreakFragment() throws { var options = Options(formatOptions: FormatOptions(fragment: true)) try options.addArguments(["indent": "2"], in: "") guard let formatOptions = options.formatOptions else { XCTFail() return } XCTAssertTrue(formatOptions.fragment) } func testAddArgumentsDoesntBreakFileInfo() throws { let fileInfo = FileInfo(filePath: "~/Foo.swift", creationDate: Date()) var options = Options(formatOptions: FormatOptions(fileInfo: fileInfo)) try options.addArguments(["indent": "2"], in: "") guard let formatOptions = options.formatOptions else { XCTFail() return } XCTAssertEqual(formatOptions.fileInfo, fileInfo) } // MARK: options parsing func testParseEmptyOptions() throws { let options = try Options([:], in: "") XCTAssertNil(options.formatOptions) XCTAssertNil(options.fileOptions) XCTAssertEqual(options.rules, defaultRules) } func testParseExcludedURLsFileOption() throws { let options = try Options(["exclude": "foo bar, baz"], in: "/dir") let paths = options.fileOptions?.excludedGlobs.map(\.description) ?? [] XCTAssertEqual(paths, ["/dir/foo bar", "/dir/baz"]) } func testParseUnexcludedURLsFileOption() throws { let options = try Options(["unexclude": "foo bar, baz"], in: "/dir") let paths = options.fileOptions?.unexcludedGlobs.map(\.description) ?? [] XCTAssertEqual(paths, ["/dir/foo bar", "/dir/baz"]) } func testParseDeprecatedOption() throws { let options = try Options(["ranges": "nospace"], in: "") XCTAssertEqual(options.formatOptions?.spaceAroundRangeOperators, .remove) } func testParseNoSpaceOperatorsOption() throws { let options = try Options(["no-space-operators": "...,..<"], in: "") XCTAssertEqual(options.formatOptions?.noSpaceOperators, ["...", "..<"]) } func testParseNoWrapOperatorsOption() throws { let options = try Options(["no-wrap-operators": ".,:,*"], in: "") XCTAssertEqual(options.formatOptions?.noWrapOperators, [".", ":", "*"]) } func testParseModifierOrderOption() throws { let options = try Options(["modifier-order": "private(set),public,unowned"], in: "") XCTAssertEqual(options.formatOptions?.modifierOrder, ["private(set)", "public", "unowned"]) } func testParseParameterizedModifierOrderOption() throws { let options = try Options(["modifier-order": "unowned(unsafe),unowned(safe)"], in: "") XCTAssertEqual(options.formatOptions?.modifierOrder, ["unowned(unsafe)", "unowned(safe)"]) } func testParseInvalidModifierOrderOption() throws { XCTAssertThrowsError(try Options(["modifier-order": "unknowned"], in: "")) { error in XCTAssertEqual("\(error)", "Unsupported --modifier-order value 'unknowned'. Did you mean 'unowned'?") } } func testParseSpecifierOrderOption() throws { let options = try Options(["specifier-order": "private(set),public"], in: "") XCTAssertEqual(options.formatOptions?.modifierOrder, ["private(set)", "public"]) } func testParseSwiftVersionOption() throws { let options = try Options(["swift-version": "4.2"], in: "") XCTAssertEqual(options.formatOptions?.swiftVersion, "4.2") } // MARK: parse rules func testParseRulesCaseInsensitive() throws { let rules = try parseRules("strongoutlets", ignoreUnknown: false) XCTAssertEqual(rules, ["strongOutlets"]) } func testParseAllRule() throws { let rules = try parseRules("all", ignoreUnknown: false) XCTAssertEqual(rules, FormatRules.all.compactMap { $0.isDeprecated ? nil : $0.name }) } func testParseInvalidRuleThrows() { XCTAssertThrowsError(try parseRules("strongOutlet", ignoreUnknown: false)) { error in XCTAssertEqual("\(error)", "Unknown rule 'strongOutlet'. Did you mean 'strongOutlets'?") } } func testParseOptionAsRuleThrows() { 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 { let options = try Options(["lint": "", "lint-only": "wrapEnumCases"], in: "") XCTAssert(options.rules?.contains("wrapEnumCases") == true) let arguments = argumentsFor(options) XCTAssertEqual(arguments, ["lint": "", "enable": "wrapEnumCases"]) } func testLintonlyRulesDontContain() throws { let options = try Options(["lint-only": "unusedArguments"], in: "") XCTAssert(options.rules?.contains("unusedArguments") == false) let arguments = argumentsFor(options) XCTAssertEqual(arguments, ["disable": "unusedArguments"]) } func testLintonlyMergeOptionsAdd() throws { var options = try Options(["lint": "", "disable": "unusedArguments"], in: "") try options.addArguments(["lint-only": "unusedArguments"], in: "") XCTAssert(options.rules?.contains("unusedArguments") == true) } func testLintonlyMergeOptionsRemove() throws { var options = try Options(["enable": "wrapEnumCases"], in: "") try options.addArguments(["lint-only": "wrapEnumCases"], in: "") XCTAssert(options.rules?.contains("wrapEnumCases") == false) } // MARK: Edit distance func testEditDistance() { XCTAssertEqual("foo".editDistance(from: "fob"), 1) XCTAssertEqual("foo".editDistance(from: "boo"), 1) XCTAssertEqual("foo".editDistance(from: "bar"), 3) XCTAssertEqual("aba".editDistance(from: "bbb"), 2) XCTAssertEqual("foob".editDistance(from: "foo"), 1) XCTAssertEqual("foo".editDistance(from: "foob"), 1) XCTAssertEqual("foo".editDistance(from: "Foo"), 1) XCTAssertEqual("FOO".editDistance(from: "foo"), 3) } func testEditDistanceWithEmptyStrings() { XCTAssertEqual("foo".editDistance(from: ""), 3) XCTAssertEqual("".editDistance(from: "foo"), 3) XCTAssertEqual("".editDistance(from: ""), 0) } // MARK: Warnings func testDeprecatedRuleWarning() { let warnings = warningsForArguments(["rules": "specifiers"]) XCTAssertEqual(warnings.count, 1) XCTAssert((warnings.first ?? "").contains("rule is deprecated")) } func testDeprecatedOptionWarning() { let warnings = warningsForArguments(["insert-lines": "enabled"]) XCTAssertEqual(warnings.count, 1) XCTAssert((warnings.first ?? "").contains("option is deprecated")) } func testUnusedOptionWarning() { let warnings = warningsForArguments([ "disable": "sortImports", "import-grouping": "testable-bottom", ]) XCTAssertEqual(warnings.count, 1) XCTAssert((warnings.first ?? "").contains("option has no effect")) } func testLintOnlyRuleDoesntTriggerUnusedOptionWarning() { let warnings = warningsForArguments([ "lintonly": "sortImports", "import-grouping": "testable-bottom", ]) XCTAssertEqual(warnings, []) } func testLintOnlyRuleDoesntTriggerUnusedOptionWarning2() throws { let options = try Options([ "lint-only": "sortImports", "import-grouping": "testable-bottom", ], in: "") let arguments = argumentsFor(options, excludingDefaults: true) // This is a bug / known issue - since --lintonly rules aren't tracked through // conversion to Options, resulting in a false positive for 'option has no effect' let warnings = warningsForArguments(arguments) XCTAssertEqual(warnings.count, 1) XCTAssert((warnings.first ?? "").contains("option has no effect")) // This is the solution to the aforementioned bug XCTAssertEqual(warningsForArguments(arguments, ignoreUnusedOptions: true), []) } }