Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check type name conflicts in SelectionSet #3009

Merged
merged 6 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */; };
5BB2C0232380836100774170 /* VersionNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB2C0222380836100774170 /* VersionNumberTests.swift */; };
66321AE72A126C4400CC35CB /* IR+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66321AE62A126C4400CC35CB /* IR+Formatting.swift */; };
96F32D3B27CCD16B00F3383C /* animalkingdom-graphql in Resources */ = {isa = PBXBuildFile; fileRef = 96F32D3A27CCD16B00F3383C /* animalkingdom-graphql */; };
96F32D3C27CCD16D00F3383C /* animalkingdom-graphql in Resources */ = {isa = PBXBuildFile; fileRef = 96F32D3A27CCD16B00F3383C /* animalkingdom-graphql */; };
9B1CCDD92360F02C007C9032 /* Bundle+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */; };
Expand Down Expand Up @@ -1133,6 +1134,7 @@
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPMethod.swift; sourceTree = "<group>"; };
5BB2C0222380836100774170 /* VersionNumberTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNumberTests.swift; sourceTree = "<group>"; };
66321AE62A126C4400CC35CB /* IR+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IR+Formatting.swift"; sourceTree = "<group>"; };
90690D05224333DA00FC2E54 /* Apollo-Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Debug.xcconfig"; sourceTree = "<group>"; };
90690D06224333DA00FC2E54 /* Apollo-Target-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-Framework.xcconfig"; sourceTree = "<group>"; };
90690D07224333DA00FC2E54 /* Apollo-Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Release.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2471,6 +2473,7 @@
9B7B6F68233C2C0C00F32205 /* FileManager+Apollo.swift */,
9BAEEBF62346F0A000808306 /* StaticString+Apollo.swift */,
9B8C3FB1248DA2EA00707B13 /* URL+Apollo.swift */,
66321AE62A126C4400CC35CB /* IR+Formatting.swift */,
);
name = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -5034,6 +5037,7 @@
9F62E03F2590896400E6E808 /* GraphQLError.swift in Sources */,
E6AAA732286BC58200F4659D /* OperationIdentifiersFileGenerator.swift in Sources */,
9B7B6F5A233C287200F32205 /* ApolloCodegenConfiguration.swift in Sources */,
66321AE72A126C4400CC35CB /* IR+Formatting.swift in Sources */,
9F1A966B258F34BB00A06EEB /* GraphQLJSFrontend.swift in Sources */,
DEB05B48289C3B4000170299 /* MockInterfacesFileGenerator.swift in Sources */,
DE09114E27288B1F000648E5 /* SortedSelections.swift in Sources */,
Expand Down
44 changes: 42 additions & 2 deletions Sources/ApolloCodegenLib/ApolloCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ApolloCodegen {
case invalidConfiguration(message: String)
case invalidSchemaName(_ name: String, message: String)
case targetNameConflict(name: String)
case typeNameConflict(name: String, conflictingName: String, containingObject: String)

public var errorDescription: String? {
switch self {
Expand Down Expand Up @@ -53,8 +54,15 @@ public class ApolloCodegen {
return "The schema namespace `\(name)` is invalid: \(message)"
case let .targetNameConflict(name):
return """
Target name '\(name)' conflicts with a reserved library name. Please choose a different \
target name.
Target name '\(name)' conflicts with a reserved library name. Please choose a different \
target name.
"""
case let .typeNameConflict(name, conflictingName, containingObject):
return """
TypeNameConflict - \
Field '\(conflictingName)' conflicts with field '\(name)' in operation/fragment `\(containingObject)`. \
Recommend using a field alias for one of these fields to resolve this conflict. \
For more info see: https://www.apollographql.com/docs/ios/troubleshooting/codegen-troubleshooting#typenameconflict
"""
}
}
Expand Down Expand Up @@ -212,6 +220,36 @@ public class ApolloCodegen {
throw Error.schemaNameConflict(name: context.schemaNamespace)
}
}

/// Validates that there are no type conflicts within a SelectionSet
static private func validateTypeConflicts(for selectionSet: IR.SelectionSet, with context: ConfigurationContext, in containingObject: String) throws {
// Check for type conflicts resulting from singularization/pluralization of fields
var fieldNamesByFormattedTypeName = [String: String]()
var fields: [IR.EntityField] = selectionSet.selections.direct?.fields.values.compactMap { $0 as? IR.EntityField } ?? []
fields.append(contentsOf: selectionSet.selections.merged.fields.values.compactMap { $0 as? IR.EntityField } )

try fields.forEach { field in
let formattedTypeName = field.formattedSelectionSetName(with: context.pluralizer)
if let existingFieldName = fieldNamesByFormattedTypeName[formattedTypeName] {
throw Error.typeNameConflict(
name: existingFieldName,
conflictingName: field.name,
containingObject: containingObject
)
}

fieldNamesByFormattedTypeName[formattedTypeName] = field.name
try validateTypeConflicts(for: field.selectionSet, with: context, in: containingObject)
}

// gather nested fragments to loop through and check as well
var nestedSelectionSets: [IR.SelectionSet] = selectionSet.selections.direct?.inlineFragments.values.elements ?? []
nestedSelectionSets.append(contentsOf: selectionSet.selections.merged.inlineFragments.values)

try nestedSelectionSets.forEach { nestedSet in
try validateTypeConflicts(for: nestedSet, with: context, in: containingObject)
}
}

/// Performs GraphQL source validation and compiles the schema and operation source documents.
static func compileGraphQLResult(
Expand Down Expand Up @@ -304,6 +342,7 @@ public class ApolloCodegen {
for fragment in compilationResult.fragments {
try autoreleasepool {
let irFragment = ir.build(fragment: fragment)
try validateTypeConflicts(for: irFragment.rootField.selectionSet, with: config, in: irFragment.definition.name)
try FragmentFileGenerator(irFragment: irFragment, config: config)
.generate(forConfig: config, fileManager: fileManager)
}
Expand All @@ -314,6 +353,7 @@ public class ApolloCodegen {
for operation in compilationResult.operations {
try autoreleasepool {
let irOperation = ir.build(operation: operation)
try validateTypeConflicts(for: irOperation.rootField.selectionSet, with: config, in: irOperation.definition.name)
try OperationFileGenerator(irOperation: irOperation, config: config)
.generate(forConfig: config, fileManager: fileManager)

Expand Down
40 changes: 40 additions & 0 deletions Sources/ApolloCodegenLib/IR+Formatting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

extension GraphQLType {

var isListType: Bool {
switch self {
case .list: return true
case let .nonNull(innerType): return innerType.isListType
case .entity, .enum, .inputObject, .scalar: return false
}
}

}

extension IR.EntityField {

/// Takes the associated ``IR.EntityField`` and formats it into a selection set name
func formattedSelectionSetName(
with pluralizer: Pluralizer
) -> String {
IR.Entity.FieldPathComponent(name: responseKey, type: type)
.formattedSelectionSetName(with: pluralizer)
}

}

extension IR.Entity.FieldPathComponent {

/// Takes the associated ``IR.Entity.FieldPathComponent`` and formats it into a selection set name
func formattedSelectionSetName(
with pluralizer: Pluralizer
) -> String {
var fieldName = name.firstUppercased
if type.isListType {
fieldName = pluralizer.singularize(fieldName)
}
return fieldName.asSelectionSetName
}

}
37 changes: 0 additions & 37 deletions Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -626,43 +626,6 @@ fileprivate extension IR.SelectionSet {

}

fileprivate extension IR.EntityField {

func formattedSelectionSetName(
with pluralizer: Pluralizer
) -> String {
IR.Entity.FieldPathComponent(name: responseKey, type: type)
.formattedSelectionSetName(with: pluralizer)
}

}

fileprivate extension IR.Entity.FieldPathComponent {

func formattedSelectionSetName(
with pluralizer: Pluralizer
) -> String {
var fieldName = name.firstUppercased
if type.isListType {
fieldName = pluralizer.singularize(fieldName)
}
return fieldName.asSelectionSetName
}

}

fileprivate extension GraphQLType {

var isListType: Bool {
switch self {
case .list: return true
case let .nonNull(innerType): return innerType.isListType
case .entity, .enum, .inputObject, .scalar: return false
}
}

}

fileprivate extension IR.MergedSelections.MergedSource {

func generatedSelectionSetName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ApolloAPI

public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
public static func cacheKeyInfo(for type: Object, object: some ObjectData) -> CacheKeyInfo? {
public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
// Implement this function to configure cache key resolution for your schema types.
return nil
}
Expand Down
Loading