diff --git a/Sources/Rules/SwiftTestingTestCaseNames.swift b/Sources/Rules/SwiftTestingTestCaseNames.swift index 8e798b7d..5f87a2c2 100644 --- a/Sources/Rules/SwiftTestingTestCaseNames.swift +++ b/Sources/Rules/SwiftTestingTestCaseNames.swift @@ -213,18 +213,47 @@ extension String { words.append(contentsOf: String(segment).splitCamelCase()) } - return words.joined(separator: " ").lowercased() + // Merge a lone single lowercase leading character with a following all-uppercase word. + // This handles acronym-first names after test prefix removal, e.g. "uUID" (from "testUUID") → "UUID". + if words.count >= 2, + words[0].count == 1, + words[0].first?.isLowercase == true, + words[1].allSatisfy(\.isUppercase) + { + words = [words[0].uppercased() + words[1]] + Array(words.dropFirst(2)) + } + + // Lowercase each word, but preserve all-uppercase words (acronyms like UUID, URL, ABC). + return words.map { $0.allSatisfy(\.isUppercase) ? $0 : $0.lowercased() }.joined(separator: " ") } - /// Splits a camelCase string into individual words. + /// Splits a camelCase string into individual words, treating consecutive uppercase letters as acronyms. + /// For example: "UUIDIsValid" → ["UUID", "Is", "Valid"], "alphabetStartsWithABC" → ["alphabet", "Starts", "With", "ABC"]. func splitCamelCase() -> [String] { var words: [String] = [] var currentWord = "" + let chars = Array(self) - for char in self { - if char.isUppercase, !currentWord.isEmpty { - words.append(currentWord) - currentWord = String(char) + for i in 0 ..< chars.count { + let char = chars[i] + let nextChar = i + 1 < chars.count ? chars[i + 1] : nil + + if char.isUppercase { + if currentWord.isEmpty { + currentWord.append(char) + } else if currentWord.last!.isLowercase { + // Lower→Upper transition: start a new word + words.append(currentWord) + currentWord = String(char) + } else if let next = nextChar, next.isLowercase { + // Uppercase sequence followed by lowercase: this char starts a new word + // e.g. "UUIDIs" → "UUID" + "Is" + words.append(currentWord) + currentWord = String(char) + } else { + // Continue accumulating the uppercase sequence (acronym) + currentWord.append(char) + } } else { currentWord.append(char) } diff --git a/Tests/Rules/SwiftTestingTestCaseNamesTests.swift b/Tests/Rules/SwiftTestingTestCaseNamesTests.swift index 3837676d..cb045885 100644 --- a/Tests/Rules/SwiftTestingTestCaseNamesTests.swift +++ b/Tests/Rules/SwiftTestingTestCaseNamesTests.swift @@ -1100,4 +1100,85 @@ final class SwiftTestingTestCaseNamesTests: XCTestCase { swiftVersion: "6.2"), exclude: [.enumNamespaces]) } + + func testConvertsAcronymAtStartToRawIdentifier() { + let input = """ + import Testing + + struct MyFeatureTests { + @Test + func testUUIDIsValid() { + #expect(true) + } + } + """ + + let output = """ + import Testing + + struct MyFeatureTests { + @Test + func `UUID is valid`() { + #expect(true) + } + } + """ + + testFormatting(for: input, output, rule: .swiftTestingTestCaseNames, + options: FormatOptions(swiftVersion: "6.2")) + } + + func testConvertsTrailingAcronymToRawIdentifier() { + let input = """ + import Testing + + struct MyFeatureTests { + @Test + func testAlphabetStartsWithABC() { + #expect(true) + } + } + """ + + let output = """ + import Testing + + struct MyFeatureTests { + @Test + func `alphabet starts with ABC`() { + #expect(true) + } + } + """ + + testFormatting(for: input, output, rule: .swiftTestingTestCaseNames, + options: FormatOptions(swiftVersion: "6.2")) + } + + func testConvertsMiddleAcronymToRawIdentifier() { + let input = """ + import Testing + + struct MyFeatureTests { + @Test + func testMyURLIsValid() { + #expect(true) + } + } + """ + + let output = """ + import Testing + + struct MyFeatureTests { + @Test + func `my URL is valid`() { + #expect(true) + } + } + """ + + testFormatting(for: input, output, rule: .swiftTestingTestCaseNames, + options: FormatOptions(swiftVersion: "6.2")) + } }