| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- // Sources/SwiftProtobufPluginLibrary/NamingUtils.swift - Utilities for generating names
- //
- // Copyright (c) 2014 - 2017 Apple Inc. and the project authors
- // Licensed under Apache License v2.0 with Runtime Library Exception
- //
- // See LICENSE.txt for license information:
- // https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
- //
- // -----------------------------------------------------------------------------
- ///
- /// This provides some utilities for generating names.
- ///
- /// NOTE: Only a very small subset of this is public. The intent is for this to
- /// expose a defined api within the PluginLib, but the the SwiftProtobufNamer
- /// to be what exposes the reusable parts at a much higher level. This reduces
- /// the changes of something being reimplemented but with minor differences.
- ///
- // -----------------------------------------------------------------------------
- import Foundation
- import SwiftProtobuf
- ///
- /// We won't generate types (structs, enums) with these names:
- ///
- private let reservedTypeNames: Set<String> = {
- () -> Set<String> in
- var names: Set<String> = []
- // Main SwiftProtobuf namespace
- // Shadowing this leads to Bad Things.
- names.insert("SwiftProtobuf")
- // Subtype of many messages, used to scope nested extensions
- names.insert("Extensions")
- // Subtypes are static references, so can conflict with static
- // class properties:
- names.insert("protoMessageName")
- // Methods on Message that we need to avoid shadowing. Testing
- // shows we do not need to avoid `serializedData` or `isEqualTo`,
- // but it's not obvious to me what's different about them. Maybe
- // because these two are generic? Because they throw?
- names.insert("decodeMessage")
- names.insert("traverse")
- // Basic Message properties we don't want to shadow:
- names.insert("isInitialized")
- names.insert("unknownFields")
- // Standard Swift property names we don't want
- // to conflict with:
- names.insert("debugDescription")
- names.insert("description")
- names.insert("dynamicType")
- names.insert("hashValue")
- // We don't need to protect all of these keywords, just the ones
- // that interfere with type expressions:
- // names = names.union(swiftKeywordsReservedInParticularContexts)
- names.insert("Type")
- names.insert("Protocol")
- // Getting something called "Swift" would be bad as it blocks access
- // to built in things.
- names.insert("Swift")
- // And getting things on some of the common protocols could create
- // some odd confusion.
- names.insert("Equatable")
- names.insert("Hashable")
- names.insert("Sendable")
- names = names.union(swiftKeywordsUsedInDeclarations)
- names = names.union(swiftKeywordsUsedInStatements)
- names = names.union(swiftKeywordsUsedInExpressionsAndTypes)
- names = names.union(swiftCommonTypes)
- names = names.union(swiftSpecialVariables)
- return names
- }()
- ///
- /// Many Swift reserved words can be used as fields names if we put backticks
- /// around them:
- ///
- private let quotableFieldNames: Set<String> = {
- () -> Set<String> in
- var names: Set<String> = []
- names = names.union(swiftKeywordsUsedInDeclarations)
- names = names.union(swiftKeywordsUsedInStatements)
- names = names.union(swiftKeywordsUsedInExpressionsAndTypes)
- return names
- }()
- private let reservedFieldNames: Set<String> = {
- () -> Set<String> in
- var names: Set<String> = []
- // Properties are instance names, so can't shadow static class
- // properties such as `protoMessageName`.
- // Properties can't shadow methods. For example, we don't need to
- // avoid `isEqualTo` as a field name.
- // Basic Message properties that we don't want to shadow
- names.insert("isInitialized")
- names.insert("unknownFields")
- // Standard Swift property names we don't want
- // to conflict with:
- names.insert("debugDescription")
- names.insert("description")
- names.insert("dynamicType")
- names.insert("hashValue")
- names.insert("init")
- names.insert("self")
- // We don't need to protect all of these keywords, just the ones
- // that interfere with type expressions:
- // names = names.union(swiftKeywordsReservedInParticularContexts)
- names.insert("Type")
- names.insert("Protocol")
- names = names.union(swiftCommonTypes)
- names = names.union(swiftSpecialVariables)
- return names
- }()
- ///
- /// Many Swift reserved words can be used as enum cases if we put quotes
- /// around them:
- ///
- private let quotableEnumCases: Set<String> = {
- () -> Set<String> in
- var names: Set<String> = []
- // We don't need to protect all of these keywords, just the ones
- // that interfere with enum cases:
- // names = names.union(swiftKeywordsReservedInParticularContexts)
- names.insert("associativity")
- names.insert("dynamicType")
- names.insert("optional")
- names.insert("required")
- names = names.union(swiftKeywordsUsedInDeclarations)
- names = names.union(swiftKeywordsUsedInStatements)
- names = names.union(swiftKeywordsUsedInExpressionsAndTypes)
- // Common type and variable names don't cause problems as enum
- // cases, because enum case names only appear in special contexts:
- // names = names.union(swiftCommonTypes)
- // names = names.union(swiftSpecialVariables)
- return names
- }()
- ///
- /// Some words cannot be used for enum cases, even if they are quoted with
- /// backticks:
- ///
- private let reservedEnumCases: Set<String> = [
- // Don't conflict with standard Swift property names:
- "allCases",
- "debugDescription",
- "description",
- "dynamicType",
- "hashValue",
- "init",
- "rawValue",
- "self",
- ]
- ///
- /// Message scoped extensions are scoped within the Message struct with `enum
- /// Extensions { ... }`, so we resuse the same sets for backticks and reserved
- /// words.
- ///
- private let quotableMessageScopedExtensionNames: Set<String> = quotableEnumCases
- private let reservedMessageScopedExtensionNames: Set<String> = reservedEnumCases
- private func isAllUnderscore(_ s: String) -> Bool {
- if s.isEmpty {
- return false
- }
- for c in s.unicodeScalars {
- if c != "_" { return false }
- }
- return true
- }
- private func sanitizeTypeName(_ s: String, disambiguator: String, forbiddenTypeNames: Set<String>) -> String {
- // NOTE: This code relies on the protoc validation of _identifier_ is defined
- // (in Tokenizer::Next() as `[a-zA-Z_][a-zA-Z0-9_]*`, so this does not need
- // any complex validation or handing of characters outside those ranges. Since
- // those rules prevent a leading digit; nothing needs to be done, and any
- // explicitly use Message or Enum name will be valid. The one exception is
- // this code is also used for determining the OneOf enums, but that code is
- // responsible for dealing with the issues in the transforms it makes.
- if reservedTypeNames.contains(s) {
- return s + disambiguator
- } else if isAllUnderscore(s) {
- return s + disambiguator
- } else if s.hasSuffix(disambiguator) {
- // If `foo` and `fooMessage` both exist, and `foo` gets
- // expanded to `fooMessage`, then we also should expand
- // `fooMessage` to `fooMessageMessage` to avoid creating a new
- // conflict. This can be resolved recursively by stripping
- // the disambiguator, sanitizing the root, then re-adding the
- // disambiguator:
- let e = s.index(s.endIndex, offsetBy: -disambiguator.count)
- let truncated = String(s[..<e])
- return sanitizeTypeName(truncated, disambiguator: disambiguator, forbiddenTypeNames: forbiddenTypeNames)
- + disambiguator
- } else if forbiddenTypeNames.contains(s) {
- // NOTE: It is important that this case runs after the hasSuffix case.
- // This set of forbidden type names is not fixed, and may contain something
- // like "FooMessage". If it does, and if s is "FooMessage with a
- // disambiguator of "Message", then we want to sanitize on the basis of
- // the suffix rather simply appending the disambiguator.
- // We use this for module imports that are configurable (like SwiftProtobuf
- // renaming).
- return s + disambiguator
- } else {
- return s
- }
- }
- private func isCharacterUppercase(_ s: String, index: Int) -> Bool {
- let scalars = s.unicodeScalars
- let start = scalars.index(scalars.startIndex, offsetBy: index)
- if start == scalars.endIndex {
- // it ended, so just say the next character wasn't uppercase.
- return false
- }
- return scalars[start].isASCUppercase
- }
- private func makeUnicodeScalarView(
- from unicodeScalar: UnicodeScalar
- ) -> String.UnicodeScalarView {
- var view = String.UnicodeScalarView()
- view.append(unicodeScalar)
- return view
- }
- private enum CamelCaser {
- // Abbreviation that should be all uppercase when camelcasing. Used in
- // camelCased(:initialUpperCase:).
- static let appreviations: Set<String> = ["url", "http", "https", "id"]
- // The diffent "classes" a character can belong in for segmenting.
- enum CharClass {
- case digit
- case lower
- case upper
- case underscore
- case other
- init(_ from: UnicodeScalar) {
- switch from {
- case "0"..."9":
- self = .digit
- case "a"..."z":
- self = .lower
- case "A"..."Z":
- self = .upper
- case "_":
- self = .underscore
- default:
- self = .other
- }
- }
- }
- /// Transforms the input into a camelcase name that is a valid Swift
- /// identifier. The input is assumed to be a protocol buffer identifier (or
- /// something like that), meaning that it is a "snake_case_name" and the
- /// underscores and be used to split into segements and then capitalize as
- /// needed. The splits happen based on underscores and/or changes in case
- /// and/or use of digits. If underscores are repeated, then the "extras"
- /// (past the first) are carried over into the output.
- ///
- /// NOTE: protoc validation of an _identifier_ is defined (in Tokenizer::Next()
- /// as `[a-zA-Z_][a-zA-Z0-9_]*`, Since leading underscores are removed, it does
- /// have to handle if things would have started with a digit. If that happens,
- /// then an underscore is added before it (which matches what the proto file
- /// would have had to have a valid identifier also).
- static func transform(_ s: String, initialUpperCase: Bool) -> String {
- var result = String()
- var current = String.UnicodeScalarView() // Collects in lowercase.
- var lastClass = CharClass("\0")
- func addCurrent() {
- guard !current.isEmpty else {
- return
- }
- var currentAsString = String(current)
- if result.isEmpty && !initialUpperCase {
- // Nothing, want it to stay lowercase.
- } else if appreviations.contains(currentAsString) {
- currentAsString = currentAsString.uppercased()
- } else {
- currentAsString = NamingUtils.uppercaseFirstCharacter(currentAsString)
- }
- result += String(currentAsString)
- current = String.UnicodeScalarView()
- }
- for scalar in s.unicodeScalars {
- let scalarClass = CharClass(scalar)
- switch scalarClass {
- case .digit:
- if lastClass != .digit {
- addCurrent()
- }
- if result.isEmpty {
- // Don't want to start with a number for the very first thing.
- result += "_"
- }
- current.append(scalar)
- case .upper:
- if lastClass != .upper {
- addCurrent()
- }
- current.append(scalar.ascLowercased())
- case .lower:
- if lastClass != .lower && lastClass != .upper {
- addCurrent()
- }
- current.append(scalar)
- case .underscore:
- addCurrent()
- if lastClass == .underscore {
- result += "_"
- }
- case .other:
- addCurrent()
- let escapeIt =
- result.isEmpty
- ? !isSwiftIdentifierHeadCharacter(scalar)
- : !isSwiftIdentifierCharacter(scalar)
- if escapeIt {
- result.append("_u\(scalar.value)")
- } else {
- current.append(scalar)
- }
- }
- lastClass = scalarClass
- }
- // Add the last segment collected.
- addCurrent()
- // If things end in an underscore, add one also.
- if lastClass == .underscore {
- result += "_"
- }
- return result
- }
- }
- // Scope for the utilies to they are less likely to conflict when imported into
- // generators.
- public enum NamingUtils {
- // Returns the type prefix to use for a given
- package static func typePrefix(protoPackage: String, fileOptions: Google_Protobuf_FileOptions) -> String {
- // Explicit option (including blank), wins.
- if fileOptions.hasSwiftPrefix {
- return fileOptions.swiftPrefix
- }
- if protoPackage.isEmpty {
- return String()
- }
- // NOTE: This code relies on the protoc validation of proto packages. Look
- // at Parser::ParsePackage() to see the logic, it comes down to reading
- // _identifiers_ joined by '.'. And _identifier_ is defined (in
- // Tokenizer::Next() as `[a-zA-Z_][a-zA-Z0-9_]*`, so this does not need
- // any complex validation or handing of characters outside those ranges.
- // It just has to deal with ended up with a leading digit after the pruning
- // of '_'s.
- // Transforms:
- // "package.name" -> "Package_Name"
- // "package_name" -> "PackageName"
- // "pacakge.some_name" -> "Package_SomeName"
- var prefix = String.UnicodeScalarView()
- var makeUpper = true
- for c in protoPackage.unicodeScalars {
- if c == "_" {
- makeUpper = true
- } else if c == "." {
- makeUpper = true
- prefix.append("_")
- } else {
- if prefix.isEmpty && c.isASCDigit {
- // If the first character is going to be a digit, add an underscore
- // to ensure it is a valid Swift identifier.
- prefix.append("_")
- }
- if makeUpper {
- prefix.append(c.ascUppercased())
- makeUpper = false
- } else {
- prefix.append(c)
- }
- }
- }
- // End in an underscore to split off anything that gets added to it.
- return String(prefix) + "_"
- }
- /// Helper a proto prefix from strings. A proto prefix means underscores
- /// and letter case are ignored.
- ///
- /// NOTE: Since this is acting on proto enum names and enum cases, we know
- /// the values must be _identifier_s which is defined (in Tokenizer::Next() as
- /// `[a-zA-Z_][a-zA-Z0-9_]*`, so this code is based on that limited input.
- package struct PrefixStripper {
- private let prefixChars: String.UnicodeScalarView
- package init(prefix: String) {
- self.prefixChars = prefix.lowercased().replacingOccurrences(of: "_", with: "").unicodeScalars
- }
- /// Strip the prefix and return the result, or return nil if it can't
- /// be stripped.
- package func strip(from: String) -> String? {
- var prefixIndex = prefixChars.startIndex
- let prefixEnd = prefixChars.endIndex
- let fromChars = from.lowercased().unicodeScalars
- var fromIndex = fromChars.startIndex
- let fromEnd = fromChars.endIndex
- while prefixIndex != prefixEnd {
- if fromIndex == fromEnd {
- // Reached the end of the string while still having prefix to go
- // nothing to strip.
- return nil
- }
- if fromChars[fromIndex] == "_" {
- fromIndex = fromChars.index(after: fromIndex)
- continue
- }
- if prefixChars[prefixIndex] != fromChars[fromIndex] {
- // They differed before the end of the prefix, can't drop.
- return nil
- }
- prefixIndex = prefixChars.index(after: prefixIndex)
- fromIndex = fromChars.index(after: fromIndex)
- }
- // Remove any more underscores.
- while fromIndex != fromEnd && fromChars[fromIndex] == "_" {
- fromIndex = fromChars.index(after: fromIndex)
- }
- if fromIndex == fromEnd {
- // They matched, can't strip.
- return nil
- }
- guard fromChars[fromIndex].isASCLowercase else {
- // Next character isn't a lowercase letter (it must be a digit
- // (fromChars was lowercased)), that would mean to make an enum value it
- // would have to get prefixed with an underscore which most folks
- // wouldn't consider to be a better Swift naming, so don't strip the
- // prefix.
- return nil
- }
- let count = fromChars.distance(from: fromChars.startIndex, to: fromIndex)
- let idx = from.index(from.startIndex, offsetBy: count)
- return String(from[idx..<from.endIndex])
- }
- }
- package static func sanitize(messageName s: String, forbiddenTypeNames: Set<String>) -> String {
- sanitizeTypeName(s, disambiguator: "Message", forbiddenTypeNames: forbiddenTypeNames)
- }
- package static func sanitize(enumName s: String, forbiddenTypeNames: Set<String>) -> String {
- sanitizeTypeName(s, disambiguator: "Enum", forbiddenTypeNames: forbiddenTypeNames)
- }
- package static func sanitize(oneofName s: String, forbiddenTypeNames: Set<String>) -> String {
- sanitizeTypeName(s, disambiguator: "Oneof", forbiddenTypeNames: forbiddenTypeNames)
- }
- package static func sanitize(fieldName s: String, basedOn: String) -> String {
- if basedOn.hasPrefix("clear") && isCharacterUppercase(basedOn, index: 5) {
- return s + "_p"
- } else if basedOn.hasPrefix("has") && isCharacterUppercase(basedOn, index: 3) {
- return s + "_p"
- } else if reservedFieldNames.contains(basedOn) {
- return s + "_p"
- } else if basedOn == s && quotableFieldNames.contains(basedOn) {
- // backticks are only used on the base names, if we're sanitizing based on something else
- // this is skipped (the "hasFoo" doesn't get backticks just because the "foo" does).
- return "`\(s)`"
- } else if isAllUnderscore(basedOn) {
- return s + "__"
- } else {
- return s
- }
- }
- package static func sanitize(fieldName s: String) -> String {
- sanitize(fieldName: s, basedOn: s)
- }
- package static func sanitize(enumCaseName s: String) -> String {
- if reservedEnumCases.contains(s) {
- return "\(s)_"
- } else if quotableEnumCases.contains(s) {
- return "`\(s)`"
- } else if isAllUnderscore(s) {
- return s + "__"
- } else {
- return s
- }
- }
- package static func sanitize(messageScopedExtensionName s: String) -> String {
- if reservedMessageScopedExtensionNames.contains(s) {
- return "\(s)_"
- } else if quotableMessageScopedExtensionNames.contains(s) {
- return "`\(s)`"
- } else if isAllUnderscore(s) {
- return s + "__"
- } else {
- return s
- }
- }
- /// Forces the first character to be uppercase (if possible) and leaves
- /// the rest of the characters in their existing case.
- ///
- /// Use toUpperCamelCase() to get leading "HTTP", "URL", etc. correct.
- package static func uppercaseFirstCharacter(_ s: String) -> String {
- let out = s.unicodeScalars
- if let first = out.first {
- var result = makeUnicodeScalarView(from: first.ascUppercased())
- result.append(
- contentsOf: out[out.index(after: out.startIndex)..<out.endIndex]
- )
- return String(result)
- } else {
- return s
- }
- }
- /// Accepts any inputs and tranforms form it into a leading
- /// UpperCaseCamelCased Swift identifier. It follows the same conventions as
- /// that are used for mapping field names into the Message property names.
- public static func toUpperCamelCase(_ s: String) -> String {
- CamelCaser.transform(s, initialUpperCase: true)
- }
- /// Accepts any inputs and tranforms form it into a leading
- /// lowerCaseCamelCased Swift identifier. It follows the same conventions as
- /// that are used for mapping field names into the Message property names.
- public static func toLowerCamelCase(_ s: String) -> String {
- CamelCaser.transform(s, initialUpperCase: false)
- }
- package static func trimBackticks(_ s: String) -> String {
- // This only has to deal with the backticks added when computing relative names, so
- // they are always matched and a single set.
- let backtick = "`"
- guard s.hasPrefix(backtick) else {
- assert(!s.hasSuffix(backtick))
- return s
- }
- assert(s.hasSuffix(backtick))
- let result = s.dropFirst().dropLast()
- assert(!result.hasPrefix(backtick) && !result.hasSuffix(backtick))
- return String(result)
- }
- static func periodsToUnderscores(_ s: String) -> String {
- s.replacingOccurrences(of: ".", with: "_")
- }
- /// This must be exactly the same as the corresponding code in the
- /// SwiftProtobuf library. Changing it will break compatibility of
- /// the generated code with old library version.
- public static func toJsonFieldName(_ s: String) -> String {
- var result = String.UnicodeScalarView()
- var capitalizeNext = false
- for c in s.unicodeScalars {
- if c == "_" {
- capitalizeNext = true
- } else if capitalizeNext {
- result.append(c.ascUppercased())
- capitalizeNext = false
- } else {
- result.append(c)
- }
- }
- return String(result)
- }
- }
|