Files
swiftpm-embedder/Sources/EmbedderTool/IdentifierSanitizer.swift
T. R. Bernstein b49d642dfd Scaffold v1.0.0
2026-04-17 01:14:06 +02:00

103 lines
3.7 KiB
Swift

import Foundation
enum IdentifierSanitizer {
static func propertyName(fromFilename filename: String) -> String {
let words = words(from: filename)
let camelCased = joinAsLowerCamelCase(words)
return escapeForSwift(camelCased)
}
static func typeName(from directoryName: String) -> String {
let words = words(from: directoryName)
let pascalCased = joinAsUpperCamelCase(words)
return escapeForSwift(pascalCased)
}
}
private extension IdentifierSanitizer {
static func words(from rawString: String) -> [String] {
let segments = splitOnNonAlphanumerics(rawString)
return segments.flatMap(splitCamelCaseBoundaries)
}
static func splitOnNonAlphanumerics(_ string: String) -> [String] {
string
.split(whereSeparator: { !$0.isLetter && !$0.isNumber })
.map(String.init)
}
static func splitCamelCaseBoundaries(_ word: String) -> [String] {
var results: [String] = []
var currentWord = ""
for character in word {
if shouldStartNewWord(at: character, currentWord: currentWord) {
results.append(currentWord)
currentWord = String(character)
} else {
currentWord.append(character)
}
}
if !currentWord.isEmpty {
results.append(currentWord)
}
return results
}
static func shouldStartNewWord(at character: Character, currentWord: String) -> Bool {
guard !currentWord.isEmpty, let lastCharacter = currentWord.last else {
return false
}
return lastCharacter.isLowercase && character.isUppercase
}
static func joinAsLowerCamelCase(_ words: [String]) -> String {
guard let firstWord = words.first else { return "" }
let remainingWords = words.dropFirst().map(capitalizeFirstLetter)
return firstWord.lowercased() + remainingWords.joined()
}
static func joinAsUpperCamelCase(_ words: [String]) -> String {
words.map(capitalizeFirstLetter).joined()
}
static func capitalizeFirstLetter(_ word: String) -> String {
guard let firstCharacter = word.first else { return "" }
return firstCharacter.uppercased() + word.dropFirst().lowercased()
}
static func escapeForSwift(_ identifier: String) -> String {
let nonEmpty = fallbackIfEmpty(identifier)
let leadingDigitSafe = prefixIfStartsWithDigit(nonEmpty)
return wrapInBackticksIfReservedKeyword(leadingDigitSafe)
}
static func fallbackIfEmpty(_ identifier: String) -> String {
identifier.isEmpty ? "_" : identifier
}
static func prefixIfStartsWithDigit(_ identifier: String) -> String {
guard let first = identifier.first, first.isNumber else {
return identifier
}
return "_" + identifier
}
static func wrapInBackticksIfReservedKeyword(_ identifier: String) -> String {
SwiftReservedKeywords.all.contains(identifier) ? "`\(identifier)`" : identifier
}
}
private enum SwiftReservedKeywords {
static let all: Set<String> = [
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate",
"func", "import", "init", "inout", "internal", "let", "open", "operator",
"private", "precedencegroup", "protocol", "public", "rethrows", "static",
"struct", "subscript", "typealias", "var",
"break", "case", "catch", "continue", "default", "defer", "do", "else",
"fallthrough", "for", "guard", "if", "in", "repeat", "return", "throw",
"switch", "where", "while",
"Any", "as", "false", "is", "nil", "self", "Self", "super", "throws",
"true", "try"
]
}