diff --git a/Rules.md b/Rules.md index 30c35a57..dc8032f0 100644 --- a/Rules.md +++ b/Rules.md @@ -1224,7 +1224,7 @@ Convert functional `forEach` calls to for loops. Option | Description --- | --- `--anonymousforeach` | Convert anonymous forEach: "convert" (default) or "ignore". -`--onelineforeach` | Convert one-line forEach: "preserve" (default) or "wrap". +`--onelineforeach` | Convert one-line forEach: "convert" or "ignore" (default).
Examples diff --git a/Sources/GrammaticalNumber.swift b/Sources/GrammaticalNumber.swift deleted file mode 100644 index cd368535..00000000 --- a/Sources/GrammaticalNumber.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// Source from v0.0.3 of the GrammaticalNumber package -// https://github.com/Cosmo/GrammaticalNumber/releases/tag/0.0.3 -// -// `public` symbols have been replaced with `internal` -// so this internal dependency isn't visible to consumers. -// - -import Foundation - -enum GrammaticalNumberRule { - case plural(_ rule: String, _ replacement: String) - case singular(_ rule: String, _ replacement: String) - case irregular(_ singular: String, _ plural: String) - case uncountable(_ rule: String) - - static var defaultLanguage = "en" - - static func add(_ rule: GrammaticalNumberRule, language: String = defaultLanguage) { - if rules[language] == nil { - rules[language] = [] - } - rules[language]?.append(rule) - } - - static func clear() { - rules = [:] - } - - static var rules: [String: [GrammaticalNumberRule]] = [ - defaultLanguage: [ - .plural(#"$"#, "s"), - .plural(#"s$"#, "s"), - .plural(#"^(ax|test)is$"#, #"$1es"#), - .plural(#"(octop|vir)us$"#, #"$1i"#), - .plural(#"(octop|vir)i$"#, #"$1i"#), - .plural(#"(alias|status)$"#, #"$1es"#), - .plural(#"(bu)s$"#, #"$1ses"#), - .plural(#"(buffal|tomat)o$"#, #"$1oes"#), - .plural(#"([ti])um$"#, #"$1a"#), - .plural(#"([ti])a$"#, #"$1a"#), - .plural(#"sis$"#, "ses"), - .plural(#"(?:([^f])fe|([lr])f)$"#, #"$1$2ves"#), - .plural(#"(hive)$"#, #"$1s"#), - .plural(#"([^aeiouy]|qu)y$"#, #"$1ies"#), - .plural(#"(x|ch|ss|sh)$"#, #"$1es"#), - .plural(#"(matr|vert|ind)(?:ix|ex)$"#, #"$1ices"#), - .plural(#"^(m|l)ouse$"#, #"$1ice"#), - .plural(#"^(m|l)ice$"#, #"$1ice"#), - .plural(#"^(ox)$"#, #"$1en"#), - .plural(#"^(oxen)$"#, #"$1"#), - .plural(#"(quiz)$"#, #"$1zes"#), - - .singular(#"s$"#, ""), - .singular(#"(ss)$"#, #"$1"#), - .singular(#"(n)ews$"#, #"$1ews"#), - .singular(#"([ti])a$"#, #"$1um"#), - .singular(#"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$"#, #"$1sis"#), - .singular(#"(^analy)(sis|ses)$"#, #"$1sis"#), - .singular(#"([^f])ves$"#, #"$1fe"#), - .singular(#"(hive)s$"#, #"$1"#), - .singular(#"(tive)s$"#, #"$1"#), - .singular(#"([lr])ves$"#, #"$1f"#), - .singular(#"([^aeiouy]|qu)ies$"#, #"$1y"#), - .singular(#"(s)eries$"#, #"$1eries"#), - .singular(#"(m)ovies$"#, #"$1ovie"#), - .singular(#"(x|ch|ss|sh)es$"#, #"$1"#), - .singular(#"^(m|l)ice$"#, #"$1ouse"#), - .singular(#"(bus)(es)?$"#, #"$1"#), - .singular(#"(o)es$"#, #"$1"#), - .singular(#"(shoe)s$"#, #"$1"#), - .singular(#"(cris|test)(is|es)$"#, #"$1is"#), - .singular(#"^(a)x[ie]s$"#, #"$1xis"#), - .singular(#"(octop|vir)(us|i)$"#, #"$1us"#), - .singular(#"(alias|status)(es)?$"#, #"$1"#), - .singular(#"^(ox)en"#, #"$1"#), - .singular(#"(vert|ind)ices$"#, #"$1ex"#), - .singular(#"(matr)ices$"#, #"$1ix"#), - .singular(#"(quiz)zes$"#, #"$1"#), - .singular(#"(database)s$"#, #"$1"#), - - .irregular("person", "people"), - .irregular("man", "men"), - .irregular("child", "children"), - .irregular("sex", "sexes"), - .irregular("move", "moves"), - .irregular("zombie", "zombies"), - .irregular("radius", "radii"), - .irregular("tooth", "teeth"), - .irregular("goose", "geese"), - - .uncountable("equipment"), - .uncountable("information"), - .uncountable("rice"), - .uncountable("money"), - .uncountable("species"), - .uncountable("series"), - .uncountable("fish"), - .uncountable("sheep"), - .uncountable("jeans"), - .uncountable("police"), - ], - ] -} - -extension String { - private func prependCount(_ count: Int?) -> String { - guard let count = count else { return self } - return [String(count), self].joined(separator: " ") - } - - func pluralized(count: Int? = nil, language: String = GrammaticalNumberRule.defaultLanguage) -> String { - var word = self - - if count == 1 { - return word.matchCase(self).prependCount(count) - } - - guard let rules = GrammaticalNumberRule.rules[language] else { - return word.matchCase(self).prependCount(count) - } - - guard let rule = (rules.reversed().first { grammaticalNumberRule -> Bool in - switch grammaticalNumberRule { - case let .uncountable(rule), let .irregular(rule, _): - return self.lowercased().contains(rule.lowercased()) - case let .plural(rule, _): - return self.range(of: rule, options: [.regularExpression, .caseInsensitive], range: nil, locale: nil) != nil - default: return false - } - }) else { - return word.matchCase(self).prependCount(count) - } - - switch rule { - case let .irregular(rule, replacement), let .plural(rule, replacement): - word = word.replacingOccurrences(of: rule, with: replacement, options: [.regularExpression, .caseInsensitive]) - default: break - } - - return word.matchCase(self).prependCount(count) - } - - func singularized(language: String = GrammaticalNumberRule.defaultLanguage) -> String { - var word = self - - guard let rules = GrammaticalNumberRule.rules[language] else { - return self - } - - guard let rule = (rules.reversed().first { grammaticalNumberRule -> Bool in - switch grammaticalNumberRule { - case let .uncountable(rule), let .irregular(_, rule): - return self.lowercased().contains(rule.lowercased()) - case let .singular(rule, _): - return self.range(of: rule, options: [.regularExpression, .caseInsensitive], range: nil, locale: nil) != nil - default: return false - } - }) else { - return word - } - - switch rule { - case let .irregular(replacement, rule), let .singular(rule, replacement): - word = word.replacingOccurrences(of: rule, with: replacement, options: [.regularExpression, .caseInsensitive]) - default: break - } - - return word.matchCase(self) - } - - func matchCase(_ input: String) -> String { - if input.allSatisfy({ $0.isUppercase }) { - return uppercased() - } else if input.allSatisfy({ $0.isLowercase }) { - return lowercased() - } else if let first = input.first, first.isUppercase { - return capitalized - } - return self - } -} diff --git a/Sources/OptionDescriptor.swift b/Sources/OptionDescriptor.swift index f042b794..51597f05 100644 --- a/Sources/OptionDescriptor.swift +++ b/Sources/OptionDescriptor.swift @@ -936,7 +936,7 @@ struct _Descriptors { let preserveSingleLineForEach = OptionDescriptor( argumentName: "onelineforeach", displayName: "Single-line forEach closures", - help: "Convert one-line forEach: \"preserve\" (default) or \"wrap\".", + help: "Convert one-line forEach: \"convert\" or \"ignore\" (default).", keyPath: \.preserveSingleLineForEach, trueValues: ["ignore", "preserve"], falseValues: ["convert"] diff --git a/Sources/Rules.swift b/Sources/Rules.swift index 93243928..0d503c47 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -7468,26 +7468,16 @@ public struct _FormatRules { return } - // We can't introduce an identifier that already exists in the for each body, + // We can't introduce an identifier that already exists in the loop body // so choose the first eligible option from a set of potential names - let eligibleValueNames: [String] - if let existingPluralIdentifier = forLoopSubjectIdentifier { - eligibleValueNames = [ - existingPluralIdentifier.singularized(), - existingPluralIdentifier.singularized() + "Item", - existingPluralIdentifier.singularized() + "Value", - "item", - "value", - ] - } else { - eligibleValueNames = ["item", "value"] + var eligibleValueNames = ["item", "element", "value"] + if let identifier = forLoopSubjectIdentifier?.singularized() { + eligibleValueNames = [identifier] + eligibleValueNames } - guard let chosenValueName = eligibleValueNames.first(where: { potentialValueName in - // The chosen name should be different than the existing plural identifier - forLoopSubjectIdentifier != potentialValueName - // And the chosen name shouldn't already exist in the closure body - && !formatter.tokens[closureOpenBraceIndex ... closureCloseBraceIndex].contains(where: { $0.string == potentialValueName }) + // The chosen name shouldn't already exist in the closure body + guard let chosenValueName = eligibleValueNames.first(where: { name in + !formatter.tokens[closureOpenBraceIndex ... closureCloseBraceIndex].contains(where: { $0.string == name }) }) else { return } forEachValueNames = [chosenValueName] diff --git a/Sources/Singularize.swift b/Sources/Singularize.swift new file mode 100644 index 00000000..fd85188e --- /dev/null +++ b/Sources/Singularize.swift @@ -0,0 +1,87 @@ +// +// Singularize.swift +// SwiftFormat +// +// Created by Nick Lockwood on 02/01/2024. +// Copyright 2024 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 + +// Inspired by https://github.com/Cosmo/GrammaticalNumber +private let rules: [(String, replacement: String)] = [ + ("s$", ""), + ("ae$", "a"), + ("eaux$", "eau"), + ("era$", "us"), + ("(e)ae$", "$1us"), + ("(ss)$", "$1"), + ("(tt?)i$", "$1o"), + ("([tivdl])a$", "$1um"), + ("(i)ves$", "$1fe"), + ("([alr]|oo|ie)ves$", "$1f"), + ("([th]ive)s$", "$1"), + ("([^aeiouy]|qu)ies$", "$1y"), + ("(ov|mb)ies$", "$1ie"), + ("(x|ch|ss|sh)es$", "$1"), + ("([ml])ice$", "$1ouse"), + ("(d)ice$", "$1ie"), + ("(bus|ato)es$", "$1"), + ("(cris|test)es$", "$1is"), + ("^(pra|a)xes$", "$1xis"), + ("([sz])es$", "$1"), + ("(z)zes$", "$1"), + ("((ba|grou)se)s$", "$1"), + ("(x)en", "$1"), + ("(oa|ly|gno|op|the|ip)ses$", "$1sis"), + ("(rt|ind|ap|cod)ices$", "$1ex"), + ("(tr|end)ices$", "$1ix"), + ("(oc|radi|vir|octop|alumn|cill|cact|fung|ul|ab)i$", "$1us"), + ("(vir)ii$", "$1us"), + ("(quiz)zes$", "$1"), + ("(pe)ople$", "$1rson"), + ("(m)en$", "$1an"), + ("(child)ren$", "$1"), + ("(t)eeth$", "$1ooth"), + ("(g)eese$", "$1oose"), + ("(iteri|en)a$", "$1on"), + // Uncountable + ("(bison|craft|deer|equipment|fish|fruit|grouse|money|news|offspring)$", "$1"), + ("(info(rmation)?|rice|salmon|series|sheep|shrimp|species|swine|trout|tuna)$", "$1"), + // It's very rare to actually want "datum" or "medium" + ("(data|media)$", "$1"), +] + +extension String { + func singularized() -> String? { + guard let (rule, replacement) = (rules.reversed().first { rule, _ in + range(of: rule, options: [.regularExpression, .caseInsensitive], range: nil, locale: nil) != nil + }) else { + return nil + } + return replacingOccurrences(of: rule, with: replacement, options: [.regularExpression, .caseInsensitive]) + } +} diff --git a/SwiftFormat.xcodeproj/project.pbxproj b/SwiftFormat.xcodeproj/project.pbxproj index b7e06026..5d4df0b0 100644 --- a/SwiftFormat.xcodeproj/project.pbxproj +++ b/SwiftFormat.xcodeproj/project.pbxproj @@ -21,13 +21,14 @@ 01426E4E23AA29B100E7D871 /* ParsingHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01426E4D23AA29B100E7D871 /* ParsingHelpersTests.swift */; }; 0142C77023C3FB6D005D5832 /* LintFileCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0142C76F23C3FB6D005D5832 /* LintFileCommand.swift */; }; 0142F06F1D72FE10007D66CC /* SwiftFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0142F06E1D72FE10007D66CC /* SwiftFormatTests.swift */; }; - 015243E22B04B0A600F65221 /* GrammaticalNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */; }; - 015243E32B04B0A600F65221 /* GrammaticalNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */; }; - 015243E42B04B0A700F65221 /* GrammaticalNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */; }; + 015243E22B04B0A600F65221 /* Singularize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* Singularize.swift */; }; + 015243E32B04B0A600F65221 /* Singularize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* Singularize.swift */; }; + 015243E42B04B0A700F65221 /* Singularize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* Singularize.swift */; }; 01567D2F225B2BFD00B22D41 /* ParsingHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01567D2E225B2BFD00B22D41 /* ParsingHelpers.swift */; }; 01567D30225B2BFD00B22D41 /* ParsingHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01567D2E225B2BFD00B22D41 /* ParsingHelpers.swift */; }; 015AF2C01DC6A538008F0A8C /* SwiftFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01A0EAA41D5DB4CF00A0A8E3 /* SwiftFormat.framework */; }; 015AF2CA1DC6A58C008F0A8C /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015AF2C91DC6A58C008F0A8C /* PerformanceTests.swift */; }; + 015CE8B12B448CCE00924504 /* SingularizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015CE8B02B448CCE00924504 /* SingularizeTests.swift */; }; 015D3A562995A0340065B2D9 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015D3A552995A0340065B2D9 /* AboutViewController.swift */; }; 018541CF1DBA0F17000F82E3 /* XCSourceTextBuffer+SwiftFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018541CE1DBA0F17000F82E3 /* XCSourceTextBuffer+SwiftFormat.swift */; }; 018E82751D62E730008CA0F8 /* TokenizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018E82741D62E730008CA0F8 /* TokenizerTests.swift */; }; @@ -77,7 +78,7 @@ 01F3DF8D1DB9FD3F00454944 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F3DF8B1DB9FD3F00454944 /* Options.swift */; }; 01F3DF8E1DB9FD3F00454944 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F3DF8B1DB9FD3F00454944 /* Options.swift */; }; 01F3DF901DBA003E00454944 /* InferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F3DF8F1DBA003E00454944 /* InferenceTests.swift */; }; - 2E7D30A42A7940C500C32174 /* GrammaticalNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */; }; + 2E7D30A42A7940C500C32174 /* Singularize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7D30A32A7940C500C32174 /* Singularize.swift */; }; 37D828AB24BF77DA0012FC0A /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37D828AA24BF77DA0012FC0A /* XcodeKit.framework */; }; 37D828AC24BF77DA0012FC0A /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 37D828AA24BF77DA0012FC0A /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9028F7831DA4B435009FE5B4 /* SwiftFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A0EAC41D5DB54A00A0A8E3 /* SwiftFormat.swift */; }; @@ -187,6 +188,7 @@ 01567D2E225B2BFD00B22D41 /* ParsingHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingHelpers.swift; sourceTree = ""; }; 015AF2C51DC6A538008F0A8C /* SwiftFormatPerfTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftFormatPerfTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 015AF2C91DC6A58C008F0A8C /* PerformanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = ""; }; + 015CE8B02B448CCE00924504 /* SingularizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingularizeTests.swift; sourceTree = ""; }; 015D3A552995A0340065B2D9 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 018541CE1DBA0F17000F82E3 /* XCSourceTextBuffer+SwiftFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCSourceTextBuffer+SwiftFormat.swift"; sourceTree = ""; }; 018E82741D62E730008CA0F8 /* TokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenizerTests.swift; sourceTree = ""; }; @@ -222,7 +224,7 @@ 01F17E841E258A4900DCD359 /* CommandLineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandLineTests.swift; sourceTree = ""; }; 01F3DF8B1DB9FD3F00454944 /* Options.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; 01F3DF8F1DBA003E00454944 /* InferenceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InferenceTests.swift; sourceTree = ""; }; - 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GrammaticalNumber.swift; sourceTree = ""; }; + 2E7D30A32A7940C500C32174 /* Singularize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Singularize.swift; sourceTree = ""; }; 37D828AA24BF77DA0012FC0A /* XcodeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeKit.framework; path = Library/Frameworks/XcodeKit.framework; sourceTree = DEVELOPER_DIR; }; 90C4B6CA1DA4B04A009EB000 /* SwiftFormat for Xcode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftFormat for Xcode.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 90C4B6CC1DA4B04A009EB000 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -359,11 +361,11 @@ 01567D2E225B2BFD00B22D41 /* ParsingHelpers.swift */, 01D3B28524E9C9C700888DE0 /* FormattingHelpers.swift */, 01ACAE04220CD90F003F3CCF /* Examples.swift */, + 2E7D30A32A7940C500C32174 /* Singularize.swift */, 01A0EAA71D5DB4CF00A0A8E3 /* SwiftFormat.h */, 01A0EAC41D5DB54A00A0A8E3 /* SwiftFormat.swift */, 01A0EABF1D5DB4F700A0A8E3 /* Tokenizer.swift */, 01BBD85821DAA2A000457380 /* Globs.swift */, - 2E7D30A32A7940C500C32174 /* GrammaticalNumber.swift */, ); path = Sources; sourceTree = ""; @@ -392,6 +394,7 @@ 01C209AE2502CD3C00E728A2 /* RulesTests+Spacing.swift */, 01EF830E25616089003F6F2D /* RulesTests+Syntax.swift */, 01C209B82502D3C500E728A2 /* RulesTests+Wrapping.swift */, + 015CE8B02B448CCE00924504 /* SingularizeTests.swift */, 0142F06E1D72FE10007D66CC /* SwiftFormatTests.swift */, 01BBD85D21DAA30700457380 /* GlobsTests.swift */, 018E82741D62E730008CA0F8 /* TokenizerTests.swift */, @@ -786,7 +789,7 @@ A3DF48252620E03600F45A5F /* JSONReporter.swift in Sources */, 01A0EAC11D5DB4F700A0A8E3 /* Rules.swift in Sources */, 01A0EAC51D5DB54A00A0A8E3 /* SwiftFormat.swift in Sources */, - 2E7D30A42A7940C500C32174 /* GrammaticalNumber.swift in Sources */, + 2E7D30A42A7940C500C32174 /* Singularize.swift in Sources */, 01B3987D1D763493009ADE61 /* Formatter.swift in Sources */, 01F17E821E25870700DCD359 /* CommandLine.swift in Sources */, 01F3DF8C1DB9FD3F00454944 /* Options.swift in Sources */, @@ -822,6 +825,7 @@ 01F3DF901DBA003E00454944 /* InferenceTests.swift in Sources */, 01C209B32502CF8300E728A2 /* RulesTests+Indentation.swift in Sources */, 01C209B12502CEF700E728A2 /* RulesTests+Linebreaks.swift in Sources */, + 015CE8B12B448CCE00924504 /* SingularizeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -843,7 +847,7 @@ A3DF48262620E03600F45A5F /* JSONReporter.swift in Sources */, 01A8320724EC7F7600A9D0EB /* FormattingHelpers.swift in Sources */, 01F17E831E25870700DCD359 /* CommandLine.swift in Sources */, - 015243E22B04B0A600F65221 /* GrammaticalNumber.swift in Sources */, + 015243E22B04B0A600F65221 /* Singularize.swift in Sources */, 01ACAE06220CD914003F3CCF /* Examples.swift in Sources */, 01A0EACD1D5DB5F500A0A8E3 /* main.swift in Sources */, DD9AD3A42999FCC8001C2C0E /* Reporter.swift in Sources */, @@ -863,7 +867,7 @@ E487211D201D885A0014845E /* RulesViewController.swift in Sources */, 01045A9B2119979400D2BE3D /* Arguments.swift in Sources */, E4872114201D3B8C0014845E /* Tokenizer.swift in Sources */, - 015243E32B04B0A600F65221 /* GrammaticalNumber.swift in Sources */, + 015243E32B04B0A600F65221 /* Singularize.swift in Sources */, E4872112201D3B860014845E /* Rules.swift in Sources */, E4962DE0203F3CD500A02013 /* OptionsStore.swift in Sources */, 01ACAE07220CD915003F3CCF /* Examples.swift in Sources */, @@ -893,7 +897,7 @@ 0142C77023C3FB6D005D5832 /* LintFileCommand.swift in Sources */, E4E4D3CC2033F17C000D7CB1 /* EnumAssociable.swift in Sources */, E4FABAD8202FEF060065716E /* OptionDescriptor.swift in Sources */, - 015243E42B04B0A700F65221 /* GrammaticalNumber.swift in Sources */, + 015243E42B04B0A700F65221 /* Singularize.swift in Sources */, 9028F7841DA4B435009FE5B4 /* Tokenizer.swift in Sources */, 90F16AFB1DA5ED9A00EB4EA1 /* CommandErrors.swift in Sources */, 01A8320924EC7F7800A9D0EB /* FormattingHelpers.swift in Sources */, diff --git a/Tests/GlobsTests.swift b/Tests/GlobsTests.swift index 9a2479e1..c61edeab 100644 --- a/Tests/GlobsTests.swift +++ b/Tests/GlobsTests.swift @@ -85,14 +85,14 @@ class GlobsTests: XCTestCase { func testExpandPathWithWildcardAtStart() { let path = "*Tests.swift" let directory = URL(fileURLWithPath: #file).deletingLastPathComponent() - XCTAssertEqual(try matchGlobs(expandGlobs(path, in: directory.path), in: directory.path).count, 14) + XCTAssertEqual(try matchGlobs(expandGlobs(path, in: directory.path), in: directory.path).count, 15) } func testExpandPathWithSubdirectoryAndWildcard() { let path = "Tests/*Tests.swift" let directory = URL(fileURLWithPath: #file) .deletingLastPathComponent().deletingLastPathComponent() - XCTAssertEqual(try matchGlobs(expandGlobs(path, in: directory.path), in: directory.path).count, 14) + XCTAssertEqual(try matchGlobs(expandGlobs(path, in: directory.path), in: directory.path).count, 15) } func testSingleWildcardDoesNotMatchDirectorySlash() { diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift index f65cca22..3b7a2183 100644 --- a/Tests/RulesTests+Syntax.swift +++ b/Tests/RulesTests+Syntax.swift @@ -3977,7 +3977,9 @@ class SyntaxTests: RulesTests { let nestedArrays = [[1, 2], [3, 4]] nestedArrays.forEach { $0.forEach { - print($0) + $0.forEach { + print($0) + } } } """ @@ -3985,8 +3987,10 @@ class SyntaxTests: RulesTests { let output = """ let nestedArrays = [[1, 2], [3, 4]] for nestedArray in nestedArrays { - for nestedArrayItem in nestedArray { - print(nestedArrayItem) + for item in nestedArray { + for item in item { + print(item) + } } } """ @@ -4005,9 +4009,9 @@ class SyntaxTests: RulesTests { let output = """ let placeholderStrings = ["foo", "bar", "baaz"] - for placeholderStringItem in placeholderStrings { - let placeholderString = placeholderStringItem.uppercased() - print(placeholderString, placeholderStringItem) + for item in placeholderStrings { + let placeholderString = item.uppercased() + print(placeholderString, item) } """ @@ -4101,8 +4105,8 @@ class SyntaxTests: RulesTests { let output = """ let bar = foo.bar - for dictionaryItem in foo.item().bar[2].dictionary["myValue"] { - print(dictionaryItem) + for item in foo.item().bar[2].dictionary["myValue"] { + print(item) } """ testFormatting(for: input, output, rule: FormatRules.preferForLoop) @@ -4135,8 +4139,8 @@ class SyntaxTests: RulesTests { let output = """ let quux = foo.bar.baaz.quux - for fooItem in foo(bar)(baaz)["item"] { - print(fooItem) + for item in foo(bar)(baaz)["item"] { + print(item) } """ testFormatting(for: input, output, rule: FormatRules.preferForLoop) diff --git a/Tests/SingularizeTests.swift b/Tests/SingularizeTests.swift new file mode 100644 index 00000000..dca1fc7d --- /dev/null +++ b/Tests/SingularizeTests.swift @@ -0,0 +1,187 @@ +// +// SingularizeTests.swift +// SwiftFormatTests +// +// Created by Nick Lockwood on 02/01/2024. +// Copyright © 2024 Nick Lockwood. All rights reserved. +// + +import XCTest +@testable import SwiftFormat + +class SingularizeTests: XCTestCase { + func testSingularization() { + XCTAssertEqual("addenda".singularized(), "addendum") + XCTAssertEqual("aircraft".singularized(), "aircraft") + XCTAssertEqual("aliases".singularized(), "alias") + XCTAssertEqual("alumnae".singularized(), "alumna") + XCTAssertEqual("alumni".singularized(), "alumnus") + XCTAssertEqual("analyses".singularized(), "analysis") + XCTAssertEqual("antennae".singularized(), "antenna") + XCTAssertEqual("antennas".singularized(), "antenna") + XCTAssertEqual("apexes".singularized(), "apex") + XCTAssertEqual("apices".singularized(), "apex") + XCTAssertEqual("appendices".singularized(), "appendix") + XCTAssertEqual("axes".singularized(), "axis") + XCTAssertEqual("bacilli".singularized(), "bacillus") + XCTAssertEqual("backhoes".singularized(), "backhoe") + XCTAssertEqual("bacteria".singularized(), "bacterium") + XCTAssertEqual("bases".singularized(), "base") + XCTAssertEqual("bison".singularized(), "bison") + XCTAssertEqual("buses".singularized(), "bus") + XCTAssertEqual("bureaux".singularized(), "bureau") + XCTAssertEqual("cacti".singularized(), "cactus") + XCTAssertEqual("centers".singularized(), "center") + XCTAssertEqual("children".singularized(), "child") + XCTAssertEqual("cilia".singularized(), "cilium") + XCTAssertEqual("codices".singularized(), "codex") + XCTAssertEqual("curricula".singularized(), "curriculum") + XCTAssertEqual("criteria".singularized(), "criterion") + XCTAssertEqual("criteria".singularized(), "criterion") + XCTAssertEqual("deer".singularized(), "deer") + XCTAssertEqual("diagnoses".singularized(), "diagnosis") + XCTAssertEqual("dice".singularized(), "die") + XCTAssertEqual("dogs".singularized(), "dog") + XCTAssertEqual("doormice".singularized(), "doormouse") + XCTAssertEqual("dwarves".singularized(), "dwarf") + XCTAssertEqual("ellipses".singularized(), "ellipsis") + XCTAssertEqual("errata".singularized(), "erratum") + XCTAssertEqual("fairies".singularized(), "fairy") + XCTAssertEqual("faxes".singularized(), "fax") + XCTAssertEqual("fezes".singularized(), "fez") + XCTAssertEqual("fezzes".singularized(), "fez") + XCTAssertEqual("fish".singularized(), "fish") + XCTAssertEqual("fishes".singularized(), "fish") + XCTAssertEqual("foci".singularized(), "focus") + XCTAssertEqual("foes".singularized(), "foe") + XCTAssertEqual("formulae".singularized(), "formula") + XCTAssertEqual("foxes".singularized(), "fox") + XCTAssertEqual("fruit".singularized(), "fruit") + XCTAssertEqual("fruits".singularized(), "fruit") + XCTAssertEqual("fungi".singularized(), "fungus") + XCTAssertEqual("funguses".singularized(), "fungus") + XCTAssertEqual("geese".singularized(), "goose") + XCTAssertEqual("genera".singularized(), "genus") + XCTAssertEqual("graffiti".singularized(), "graffito") + XCTAssertEqual("grandchildren".singularized(), "grandchild") + XCTAssertEqual("grouse".singularized(), "grouse") + XCTAssertEqual("grouses".singularized(), "grouse") + XCTAssertEqual("halves".singularized(), "half") + XCTAssertEqual("hooves".singularized(), "hoof") + XCTAssertEqual("indices".singularized(), "index") + XCTAssertEqual("items".singularized(), "item") + XCTAssertEqual("knives".singularized(), "knife") + XCTAssertEqual("larvae".singularized(), "larva") + XCTAssertEqual("larvas".singularized(), "larva") + XCTAssertEqual("leaves".singularized(), "leaf") + XCTAssertEqual("libretti".singularized(), "libretto") + XCTAssertEqual("librettos".singularized(), "libretto") + XCTAssertEqual("lives".singularized(), "life") + XCTAssertEqual("loaves".singularized(), "loaf") + XCTAssertEqual("loci".singularized(), "locus") + XCTAssertEqual("matrices".singularized(), "matrix") + XCTAssertEqual("matrixes".singularized(), "matrix") + XCTAssertEqual("memoranda".singularized(), "memorandum") + XCTAssertEqual("mice".singularized(), "mouse") + XCTAssertEqual("millennia".singularized(), "millennium") + XCTAssertEqual("minutiae".singularized(), "minutia") + XCTAssertEqual("moves".singularized(), "move") + XCTAssertEqual("movies".singularized(), "movie") + XCTAssertEqual("noobies".singularized(), "nooby") + XCTAssertEqual("nebulae".singularized(), "nebula") + XCTAssertEqual("nucleae".singularized(), "nucleus") + XCTAssertEqual("oases".singularized(), "oasis") + XCTAssertEqual("octopuses".singularized(), "octopus") + XCTAssertEqual("octopi".singularized(), "octopus") + XCTAssertEqual("offspring".singularized(), "offspring") + XCTAssertEqual("opera".singularized(), "opus") + XCTAssertEqual("opuses".singularized(), "opus") + XCTAssertEqual("ova".singularized(), "ovum") + XCTAssertEqual("oxen".singularized(), "ox") + XCTAssertEqual("parentheses".singularized(), "parenthesis") + XCTAssertEqual("people".singularized(), "person") + XCTAssertEqual("phenomena".singularized(), "phenomenon") + XCTAssertEqual("phenomenons".singularized(), "phenomenon") + XCTAssertEqual("phyla".singularized(), "phylum") + XCTAssertEqual("potatoes".singularized(), "potato") + XCTAssertEqual("praxes".singularized(), "praxis") + XCTAssertEqual("quizzes".singularized(), "quiz") + XCTAssertEqual("radii".singularized(), "radius") + XCTAssertEqual("referenda".singularized(), "referendum") + XCTAssertEqual("referendums".singularized(), "referendum") + XCTAssertEqual("salmon".singularized(), "salmon") + XCTAssertEqual("scarfs".singularized(), "scarf") + XCTAssertEqual("scarves".singularized(), "scarf") + XCTAssertEqual("selves".singularized(), "self") + XCTAssertEqual("series".singularized(), "series") + XCTAssertEqual("sheep".singularized(), "sheep") + XCTAssertEqual("shoes".singularized(), "shoe") + XCTAssertEqual("shrimp".singularized(), "shrimp") + XCTAssertEqual("shrimps".singularized(), "shrimp") + XCTAssertEqual("sofas".singularized(), "sofa") + XCTAssertEqual("species".singularized(), "species") + XCTAssertEqual("statuses".singularized(), "status") + XCTAssertEqual("stimuli".singularized(), "stimulus") + XCTAssertEqual("strata".singularized(), "stratum") + XCTAssertEqual("syllabi".singularized(), "syllabus") + XCTAssertEqual("syllabuses".singularized(), "syllabus") + XCTAssertEqual("symposia".singularized(), "symposium") + XCTAssertEqual("symposiums".singularized(), "symposium") + XCTAssertEqual("synopses".singularized(), "synopsis") + XCTAssertEqual("swine".singularized(), "swine") + XCTAssertEqual("tableaux".singularized(), "tableau") + XCTAssertEqual("taxes".singularized(), "tax") + XCTAssertEqual("teeth".singularized(), "tooth") + XCTAssertEqual("theses".singularized(), "thesis") + XCTAssertEqual("thieves".singularized(), "thief") + XCTAssertEqual("toes".singularized(), "toe") + XCTAssertEqual("tomatoes".singularized(), "tomato") + XCTAssertEqual("trout".singularized(), "trout") + XCTAssertEqual("trouts".singularized(), "trout") + XCTAssertEqual("tuna".singularized(), "tuna") + XCTAssertEqual("tunas".singularized(), "tuna") + XCTAssertEqual("vertebrae".singularized(), "vertebra") + XCTAssertEqual("vertebras".singularized(), "vertebra") + XCTAssertEqual("vertices".singularized(), "vertex") + XCTAssertEqual("villae".singularized(), "villa") + XCTAssertEqual("viruses".singularized(), "virus") + XCTAssertEqual("virii".singularized(), "virus") + XCTAssertEqual("viri".singularized(), "virus") + XCTAssertEqual("vitae".singularized(), "vita") + XCTAssertEqual("vortexes".singularized(), "vortex") + XCTAssertEqual("vortices".singularized(), "vortex") + XCTAssertEqual("wharfs".singularized(), "wharf") + XCTAssertEqual("wharves".singularized(), "wharf") + XCTAssertEqual("wives".singularized(), "wife") + XCTAssertEqual("wolves".singularized(), "wolf") + XCTAssertEqual("women".singularized(), "woman") + XCTAssertEqual("zombies".singularized(), "zombie") + } + + func testPreserveCase() { + XCTAssertEqual("Axes".singularized(), "Axis") + XCTAssertEqual("COMPUTERS".singularized(), "COMPUTER") + XCTAssertEqual("Teeth".singularized(), "Tooth") + } + + func testStripAllPrefix() { + XCTAssertEqual("allWindows".singularized(), "window") + XCTAssertEqual("AllTargets".singularized(), "Target") + } + + func testDontUseDatumOrMedium() { + XCTAssertEqual("data".singularized(), "data") + XCTAssertEqual("media".singularized(), "media") + XCTAssertEqual("allMedia".singularized(), "media") + } + + func testDontSingularizeNonPlurals() { + XCTAssertNil("uppercase".singularized()) + XCTAssertNil("uppercased".singularized()) + XCTAssertNil("rotate".singularized()) + XCTAssertNil("map".singularized()) + XCTAssertNil("filter".singularized()) + XCTAssertNil("capitalize".singularized()) + XCTAssertNil("tessellate".singularized()) + } +} diff --git a/Tests/SwiftFormatTests.swift b/Tests/SwiftFormatTests.swift index b4e33c3b..0df8b0dd 100644 --- a/Tests/SwiftFormatTests.swift +++ b/Tests/SwiftFormatTests.swift @@ -67,7 +67,7 @@ class SwiftFormatTests: XCTestCase { return { files.append(inputURL) } } XCTAssertEqual(errors.count, 0) - XCTAssertEqual(files.count, 68) + XCTAssertEqual(files.count, 69) } func testInputFilesMatchOutputFilesForSameOutput() { @@ -78,7 +78,7 @@ class SwiftFormatTests: XCTestCase { return { files.append(inputURL) } } XCTAssertEqual(errors.count, 0) - XCTAssertEqual(files.count, 68) + XCTAssertEqual(files.count, 69) } func testInputFileNotEnumeratedWhenExcluded() {