Skip to content

Commit

Permalink
fix: Selection set field and fragment type name conflicts (#3041)
Browse files Browse the repository at this point in the history
  • Loading branch information
BobaFetters authored May 25, 2023
1 parent fe0bdd7 commit bafb3e9
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 7 deletions.
51 changes: 44 additions & 7 deletions Sources/ApolloCodegenLib/ApolloCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,32 +222,69 @@ public class ApolloCodegen {
}

/// 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 {
static private func validateTypeConflicts(
for selectionSet: IR.SelectionSet,
with context: ConfigurationContext,
in containingObject: String,
including parentTypes: [String: String] = [:]
) throws {
// Check for type conflicts resulting from singularization/pluralization of fields
var fieldNamesByFormattedTypeName = [String: String]()
var typeNamesByFormattedTypeName = [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] {
if let existingFieldName = typeNamesByFormattedTypeName[formattedTypeName] {
throw Error.typeNameConflict(
name: existingFieldName,
conflictingName: field.name,
containingObject: containingObject
)
}

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

// Combine `parentTypes` and `typeNamesByFormattedTypeName` to check against fragment names and
// pass into recursive function calls
var combinedTypeNames = parentTypes
combinedTypeNames.merge(typeNamesByFormattedTypeName) { (current, _) in current }

// passing each fields selection set for validation after we have fully built our `typeNamesByFormattedTypeName` dictionary
try fields.forEach { field in
try validateTypeConflicts(
for: field.selectionSet,
with: context,
in: containingObject,
including: combinedTypeNames
)
}

var fragments: [IR.NamedFragment] = selectionSet.selections.direct?.fragments.values.map { $0.fragment } ?? []
fragments.append(contentsOf: selectionSet.selections.merged.fragments.values.map { $0.fragment })

try fragments.forEach { fragment in
if let existingTypeName = combinedTypeNames[fragment.generatedDefinitionName] {
throw Error.typeNameConflict(
name: existingTypeName,
conflictingName: fragment.name,
containingObject: 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)
try validateTypeConflicts(
for: nestedSet,
with: context,
in: containingObject,
including: combinedTypeNames
)
}
}

Expand Down
241 changes: 241 additions & 0 deletions Tests/ApolloCodegenTests/ApolloCodegenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,247 @@ class ApolloCodegenTests: XCTestCase {
expect(containingObject).to(equal("ContainerFields"))
})
}

func test__validation__selectionSet_typeConflicts_withNamedFragmentFieldCollisionWithinInlineFragment_shouldThrowError() throws {
let schemaDefData: Data = {
"""
type Query {
user: User
}
type User {
containers: [ContainerInterface]
}
interface ContainerInterface {
value: Value
}
type Container implements ContainerInterface{
value: Value
values: [Value]
user: Int
}
type Value {
propertyA: String!
propertyB: String!
propertyC: String!
propertyD: String!
}
"""
}().data(using: .utf8)!

let operationData: Data =
"""
query ConflictingQuery {
user {
containers {
value {
propertyA
propertyB
propertyC
propertyD
}
... on Container {
...ValueFragment
}
}
}
}
fragment ValueFragment on Container {
values {
propertyA
propertyC
}
}
""".data(using: .utf8)!

try createFile(containing: schemaDefData, named: "schema.graphqls")
try createFile(containing: operationData, named: "operation.graphql")

let config = ApolloCodegenConfiguration.mock(
input: .init(
schemaSearchPaths: ["schema*.graphqls"],
operationSearchPaths: ["*.graphql"]
),
output: .init(
schemaTypes: .init(path: "SchemaModule",
moduleType: .swiftPackageManager),
operations: .inSchemaModule
)
)

expect(try ApolloCodegen.build(with: config))
.to(throwError { error in
guard case let ApolloCodegen.Error.typeNameConflict(name, conflictingName, containingObject) = error else {
fail("Expected .typeNameConflict, got .\(error)")
return
}
expect(name).to(equal("value"))
expect(conflictingName).to(equal("values"))
expect(containingObject).to(equal("ConflictingQuery"))
})
}

func test__validation__selectionSet_typeConflicts_withNamedFragmentWithinInlineFragmentTypeCollision_shouldThrowError() throws {
let schemaDefData: Data = {
"""
type Query {
user: User
}
type User {
containers: [ContainerInterface]
}
interface ContainerInterface {
value: Value
}
type Container implements ContainerInterface{
nestedContainer: NestedContainer
value: Value
values: [Value]
user: Int
}
type Value {
propertyA: String!
propertyB: String!
propertyC: String!
propertyD: String!
}
type NestedContainer {
values: [Value]
description: String
}
"""
}().data(using: .utf8)!

let operationData: Data =
"""
query ConflictingQuery {
user {
containers {
value {
propertyA
propertyB
propertyC
propertyD
}
... on Container {
nestedContainer {
...value
}
}
}
}
}
fragment value on NestedContainer {
description
}
""".data(using: .utf8)!

try createFile(containing: schemaDefData, named: "schema.graphqls")
try createFile(containing: operationData, named: "operation.graphql")

let config = ApolloCodegenConfiguration.mock(
input: .init(
schemaSearchPaths: ["schema*.graphqls"],
operationSearchPaths: ["*.graphql"]
),
output: .init(
schemaTypes: .init(path: "SchemaModule",
moduleType: .swiftPackageManager),
operations: .inSchemaModule
)
)

expect(try ApolloCodegen.build(with: config))
.to(throwError { error in
guard case let ApolloCodegen.Error.typeNameConflict(name, conflictingName, containingObject) = error else {
fail("Expected .typeNameConflict, got .\(error)")
return
}
expect(name).to(equal("value"))
expect(conflictingName).to(equal("value"))
expect(containingObject).to(equal("ConflictingQuery"))
})
}

func test__validation__selectionSet_typeConflicts_withFieldUsingNamedFragmentCollision_shouldThrowError() throws {
let schemaDefData: Data = {
"""
type Query {
user: User
}
type User {
containers: [Container]
}
type Container {
info: Value
}
type Value {
propertyA: String!
propertyB: String!
propertyC: String!
propertyD: String!
}
"""
}().data(using: .utf8)!

let operationData: Data =
"""
query ConflictingQuery {
user {
containers {
info {
...Info
}
}
}
}
fragment Info on Value {
propertyA
propertyB
propertyD
}
""".data(using: .utf8)!

try createFile(containing: schemaDefData, named: "schema.graphqls")
try createFile(containing: operationData, named: "operation.graphql")

let config = ApolloCodegenConfiguration.mock(
input: .init(
schemaSearchPaths: ["schema*.graphqls"],
operationSearchPaths: ["*.graphql"]
),
output: .init(
schemaTypes: .init(path: "SchemaModule",
moduleType: .swiftPackageManager),
operations: .inSchemaModule
)
)

expect(try ApolloCodegen.build(with: config))
.to(throwError { error in
guard case let ApolloCodegen.Error.typeNameConflict(name, conflictingName, containingObject) = error else {
fail("Expected .typeNameConflict, got .\(error)")
return
}
expect(name).to(equal("info"))
expect(conflictingName).to(equal("Info"))
expect(containingObject).to(equal("ConflictingQuery"))
})
}

// MARK: Path Match Exclusion Tests

Expand Down

0 comments on commit bafb3e9

Please sign in to comment.