diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift index 46a470f12..f5c157c58 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift @@ -1757,7 +1757,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 24, ignoringExtraLines: true)) } - // MARK: - Include/Skip Tests + // MARK: Include/Skip Tests func test__render_given_fieldWithInclusionCondition_rendersInitializerWithOptionalParameter() async throws { // given @@ -2136,4 +2136,110 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { expect(allAnimals_ifA_actual).to(equalLineByLine( allAnimals_ifA_expected, atLine: 23, ignoringExtraLines: true)) } + + // MARK: Parameter Name Tests + + func test__render__givenReservedFieldName_shouldGenerateParameterNameWithAlias() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + name: String + self: String # <- reserved name + } + """ + + document = """ + query TestOperation { + allAnimals { + name + self + } + } + """ + + let expected = + """ + public init( + name: String? = nil, + `self` _self: String? = nil + ) { + self.init(_dataDict: DataDict( + data: [ + "__typename": TestSchema.Objects.Animal.typename, + "name": name, + "self": _self, + ], + fulfilledFragments: [ + ObjectIdentifier(TestOperationQuery.Data.AllAnimal.self) + ] + )) + } + """ + + // when + try await buildSubjectAndOperation() + + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?.selectionSet + ) + + let actual = subject.test_render(childEntity: allAnimals.computed) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 18, ignoringExtraLines: true)) + } + + func test__render__givenFieldName_generatesParameterNameWithoutAlias() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + name + } + } + """ + + let expected = + """ + public init( + name: String? = nil + ) { + self.init(_dataDict: DataDict( + data: [ + "__typename": TestSchema.Objects.Animal.typename, + "name": name, + ], + fulfilledFragments: [ + ObjectIdentifier(TestOperationQuery.Data.AllAnimal.self) + ] + )) + } + """ + + // when + try await buildSubjectAndOperation() + + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?.selectionSet + ) + + let actual = subject.test_render(childEntity: allAnimals.computed) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + } } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/String+SwiftNameEscaping.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/String+SwiftNameEscaping.swift index 4e18edefa..2c6b557e2 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/String+SwiftNameEscaping.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/String+SwiftNameEscaping.swift @@ -34,25 +34,59 @@ extension String { private func escapeIf(in set: Set) -> String { set.contains(self) ? "`\(self)`" : self } - - /// Renders the string as the property name for a field accessor on a generated `SelectionSet`. - /// This escapes the names of properties that would conflict with Swift reserved keywords. - func renderAsFieldPropertyName( - config: ApolloCodegenConfiguration - ) -> String { + + private func aliasIf(in set: Set) -> String { + set.contains(self) ? "_\(self)" : self + } + + private func escapeWithAliasIf(in set: Set) -> String { + set.contains(self) ? "`\(self)` _\(self)" : self + } + + private func renderedAsPropertyName(config: ApolloCodegenConfiguration) -> String { var propertyName = self - + switch config.options.conversionStrategies.fieldAccessors { case .camelCase: propertyName = propertyName.convertToCamelCase() case .idiomatic: break } - + propertyName = propertyName.isAllUppercased ? propertyName.lowercased() : propertyName.firstLowercased - return propertyName.escapeIf(in: SwiftKeywords.FieldAccessorNamesToEscape) + return propertyName } - + + /// Renders the string as the property name for a field accessor on a generated `SelectionSet`. + /// This escapes the names of properties that would conflict with Swift reserved keywords. + func renderAsFieldPropertyName(config: ApolloCodegenConfiguration) -> String { + let propertyName = renderedAsPropertyName(config: config) + .escapeIf(in: SwiftKeywords.FieldAccessorNamesToEscape) + + return propertyName + } + + /// Renders the string as the parameter name for an initializer on a generated `SelectionSet`. + /// This escapes the names of parameters that would conflict with Swift reserved keywords and + /// makes them available under a parameter name alias to avoid further conflicts; see issue #3330 + /// as an example. + func renderAsInitializerParameterName(config: ApolloCodegenConfiguration) -> String { + let propertyName = renderedAsPropertyName(config: config) + .escapeWithAliasIf(in: SwiftKeywords.FieldAccessorNamesToEscape) + + return propertyName + } + + /// Renders the string as the parameter name for an initializer on a generated `SelectionSet`. + /// This underscores the names of initializer parameters that would conflict with Swift reserved + /// keywords; see issue #3330 as an example. + func renderAsInitializerParameterAccessorName(config: ApolloCodegenConfiguration) -> String { + let propertyName = renderedAsPropertyName(config: config) + .aliasIf(in: SwiftKeywords.FieldAccessorNamesToEscape) + + return propertyName + } + func renderAsInputObjectName( config: ApolloCodegenConfiguration ) -> String { diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index a4eaa45e1..71e841f60 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -608,7 +608,8 @@ struct SelectionSetTemplate { ) -> TemplateString { let isOptional: Bool = field.type.isNullable || field.isConditionallyIncluded(in: scope) return """ - \(field.responseKey.renderAsFieldPropertyName(config: config.config)): \(typeName(for: field, forceOptional: isOptional))\ + \(field.responseKey.renderAsInitializerParameterName(config: config.config)): \ + \(typeName(for: field, forceOptional: isOptional))\ \(if: isOptional, " = nil") """ } @@ -641,7 +642,7 @@ struct SelectionSetTemplate { }() return """ - "\(field.responseKey)": \(field.responseKey.renderAsFieldPropertyName(config: config.config))\ + "\(field.responseKey)": \(field.responseKey.renderAsInitializerParameterAccessorName(config: config.config))\ \(if: isEntityField, "._fieldData") """ }