From 4147f782f0b6c094f1e43df9e51b45d3c6d45e02 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 3 Aug 2023 16:36:21 -0700 Subject: [PATCH] refactor: Fragment types in IR (#3174) Co-authored-by: Anthony Miller --- Sources/ApolloCodegenLib/ApolloCodegen.swift | 12 +- .../IR/IR+EntitySelectionTree.swift | 51 ++++---- .../IR/IR+InclusionConditions.swift | 2 +- .../IR/IR+RootFieldBuilder.swift | 33 +++--- .../ApolloCodegenLib/IR/IR+SelectionSet.swift | 14 ++- Sources/ApolloCodegenLib/IR/IR.swift | 88 ++++++++++++-- .../ApolloCodegenLib/IR/ScopeDescriptor.swift | 16 ++- .../IR/ScopedSelectionSetHashable.swift | 2 +- .../IR/SortedSelections.swift | 111 +++++++++--------- .../Templates/SelectionSetTemplate.swift | 34 +++--- .../MockIRSubscripts.swift | 31 ++--- .../MockMergedSelections.swift | 4 +- .../CodeGenIR/IRRootFieldBuilderTests.swift | 19 ++- .../TestHelpers/IRMatchers.swift | 22 ++-- 14 files changed, 275 insertions(+), 164 deletions(-) diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index 93e0f2542..9d0d50ad8 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -292,10 +292,10 @@ public class ApolloCodegen { ) } - var fragments: [IR.NamedFragment] = selectionSet.selections.direct?.fragments.values.map { $0.fragment } ?? [] - fragments.append(contentsOf: selectionSet.selections.merged.fragments.values.map { $0.fragment }) + var namedFragments: [IR.NamedFragment] = selectionSet.selections.direct?.namedFragments.values.map(\.fragment) ?? [] + namedFragments.append(contentsOf: selectionSet.selections.merged.namedFragments.values.map(\.fragment)) - try fragments.forEach { fragment in + try namedFragments.forEach { fragment in if let existingTypeName = combinedTypeNames[fragment.generatedDefinitionName] { throw Error.typeNameConflict( name: existingTypeName, @@ -305,9 +305,9 @@ public class ApolloCodegen { } } - // 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) + // gather nested fragments to loop through and check as well + var nestedSelectionSets: [IR.SelectionSet] = selectionSet.selections.direct?.inlineFragments.values.map(\.selectionSet) ?? [] + nestedSelectionSets.append(contentsOf: selectionSet.selections.merged.inlineFragments.values.map(\.selectionSet)) try nestedSelectionSets.forEach { nestedSet in try validateTypeConflicts( diff --git a/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift b/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift index cc2a0a424..6e1031524 100644 --- a/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift +++ b/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift @@ -34,7 +34,7 @@ extension IR { } private func mergeIn(selections: DirectSelections.ReadOnly, from source: MergedSelections.MergedSource) { - guard (!selections.fields.isEmpty || !selections.fragments.isEmpty) else { + guard (!selections.fields.isEmpty || !selections.namedFragments.isEmpty) else { return } @@ -255,7 +255,8 @@ extension IR { fileprivate func scopeConditionNode(for condition: ScopeCondition) -> EntityNode { let nodeCondition = ScopeCondition( type: condition.type == self.type ? nil : condition.type, - conditions: condition.conditions + conditions: condition.conditions, + isDeferred: condition.isDeferred ) func createNode() -> EntityNode { @@ -291,20 +292,20 @@ extension IR { class EntityTreeScopeSelections: Equatable { fileprivate(set) var fields: OrderedDictionary = [:] - fileprivate(set) var fragments: OrderedDictionary = [:] + fileprivate(set) var namedFragments: OrderedDictionary = [:] init() {} fileprivate init( fields: OrderedDictionary, - fragments: OrderedDictionary + namedFragments: OrderedDictionary ) { self.fields = fields - self.fragments = fragments + self.namedFragments = namedFragments } var isEmpty: Bool { - fields.isEmpty && fragments.isEmpty + fields.isEmpty && namedFragments.isEmpty } private func mergeIn(_ field: Field) { @@ -315,27 +316,27 @@ extension IR { fields.forEach { mergeIn($0) } } - private func mergeIn(_ fragment: FragmentSpread) { - fragments[fragment.hashForSelectionSetScope] = fragment + private func mergeIn(_ fragment: NamedFragmentSpread) { + namedFragments[fragment.hashForSelectionSetScope] = fragment } - private func mergeIn(_ fragments: T) where T.Element == FragmentSpread { + private func mergeIn(_ fragments: T) where T.Element == NamedFragmentSpread { fragments.forEach { mergeIn($0) } } func mergeIn(_ selections: DirectSelections.ReadOnly) { mergeIn(selections.fields.values) - mergeIn(selections.fragments.values) + mergeIn(selections.namedFragments.values) } func mergeIn(_ selections: EntityTreeScopeSelections) { mergeIn(selections.fields.values) - mergeIn(selections.fragments.values) + mergeIn(selections.namedFragments.values) } static func == (lhs: IR.EntityTreeScopeSelections, rhs: IR.EntityTreeScopeSelections) -> Bool { lhs.fields == rhs.fields && - lhs.fragments == rhs.fragments + lhs.namedFragments == rhs.namedFragments } } } @@ -352,7 +353,7 @@ extension IR.EntitySelectionTree { /// programming error and will result in undefined behavior. func mergeIn( _ otherTree: IR.EntitySelectionTree, - from fragment: IR.FragmentSpread, + from fragment: IR.NamedFragmentSpread, using entityStorage: IR.RootFieldEntityStorage ) { let otherTreeCount = otherTree.rootTypePath.count @@ -391,7 +392,7 @@ extension IR.EntitySelectionTree.EntityNode { fileprivate func mergeIn( _ fragmentTree: IR.EntitySelectionTree, - from fragment: IR.FragmentSpread, + from fragment: IR.NamedFragmentSpread, with inclusionConditions: AnyOf?, using entityStorage: IR.RootFieldEntityStorage ) { @@ -407,7 +408,8 @@ extension IR.EntitySelectionTree.EntityNode { for conditionGroup in inclusionConditions.elements { let scope = IR.ScopeCondition( type: rootTypesMatch ? nil : fragmentType, - conditions: conditionGroup + conditions: conditionGroup, + isDeferred: fragment.isDeferred ) let nextNode = rootNodeToStartMerge.scopeConditionNode(for: scope) @@ -421,7 +423,9 @@ extension IR.EntitySelectionTree.EntityNode { } else { let nextNode = rootTypesMatch ? rootNodeToStartMerge : - rootNodeToStartMerge.scopeConditionNode(for: IR.ScopeCondition(type: fragmentType)) + rootNodeToStartMerge.scopeConditionNode( + for: IR.ScopeCondition(type: fragmentType, isDeferred: fragment.isDeferred) + ) nextNode.mergeIn( fragmentTree.rootNode, @@ -433,7 +437,7 @@ extension IR.EntitySelectionTree.EntityNode { fileprivate func mergeIn( _ otherNode: IR.EntitySelectionTree.EntityNode, - from fragment: IR.FragmentSpread, + from fragment: IR.NamedFragmentSpread, using entityStorage: IR.RootFieldEntityStorage ) { switch otherNode.child { @@ -470,7 +474,7 @@ extension IR.EntitySelectionTree.EntityNode { fileprivate func mergeIn( _ selections: Selections, - from fragment: IR.FragmentSpread, + from fragment: IR.NamedFragmentSpread, using entityStorage: IR.RootFieldEntityStorage ) { for (source, selections) in selections { @@ -501,23 +505,24 @@ extension IR.EntitySelectionTree.EntityNode { } } - let fragments = selections.fragments.mapValues { oldFragment -> IR.FragmentSpread in + let fragments = selections.namedFragments.mapValues { oldFragment -> IR.NamedFragmentSpread in let entity = entityStorage.entity( for: oldFragment.typeInfo.entity, inFragmentSpreadAtTypePath: fragment.typeInfo ) - return IR.FragmentSpread( + return IR.NamedFragmentSpread( fragment: oldFragment.fragment, typeInfo: IR.SelectionSet.TypeInfo( entity: entity, scopePath: oldFragment.typeInfo.scopePath ), - inclusionConditions: oldFragment.inclusionConditions + inclusionConditions: oldFragment.inclusionConditions, + isDeferred: oldFragment.isDeferred ) } self.mergeIn( - IR.EntityTreeScopeSelections(fields: fields, fragments: fragments), + IR.EntityTreeScopeSelections(fields: fields, namedFragments: fragments), from: newSource ) } @@ -577,7 +582,7 @@ extension IR.EntityTreeScopeSelections: CustomDebugStringConvertible { var debugDescription: String { """ Fields: \(fields.values.elements) - Fragments: \(fragments.values.elements.description) + Fragments: \(namedFragments.values.elements.description) """ } } diff --git a/Sources/ApolloCodegenLib/IR/IR+InclusionConditions.swift b/Sources/ApolloCodegenLib/IR/IR+InclusionConditions.swift index de7a46639..673aeb30b 100644 --- a/Sources/ApolloCodegenLib/IR/IR+InclusionConditions.swift +++ b/Sources/ApolloCodegenLib/IR/IR+InclusionConditions.swift @@ -1,7 +1,7 @@ import OrderedCollections extension IR { - + /// A condition representing an `@include` or `@skip` directive to determine if a field /// or fragment should be included. struct InclusionCondition: Hashable, CustomDebugStringConvertible { diff --git a/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift b/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift index feac095df..53707de1e 100644 --- a/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift +++ b/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift @@ -56,7 +56,7 @@ extension IR { return entity } - fileprivate func mergeAllSelectionsIntoEntitySelectionTrees(from fragmentSpread: FragmentSpread) { + fileprivate func mergeAllSelectionsIntoEntitySelectionTrees(from fragmentSpread: NamedFragmentSpread) { for (_, fragmentEntity) in fragmentSpread.fragment.entities { let entity = entity(for: fragmentEntity, inFragmentSpreadAtTypePath: fragmentSpread.typeInfo) entity.selectionTree.mergeIn(fragmentEntity.selectionTree, from: fragmentSpread, using: self) @@ -179,7 +179,7 @@ extension IR { ) } else { - let irTypeCase = buildConditionalSelectionSet( + let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, with: scope, inParentTypePath: typeInfo @@ -200,7 +200,7 @@ extension IR { let matchesScope = selectionSetScope.matches(scope) if matchesScope { - let irFragmentSpread = buildFragmentSpread( + let irFragmentSpread = buildNamedFragmentSpread( fromFragment: fragmentSpread, with: scope, spreadIntoParentWithTypePath: typeInfo @@ -208,7 +208,7 @@ extension IR { target.mergeIn(irFragmentSpread) } else { - let irTypeCaseEnclosingFragment = buildConditionalSelectionSet( + let irTypeCaseEnclosingFragment = buildInlineFragmentSpread( from: CompilationResult.SelectionSet( parentType: fragmentSpread.parentType, selections: [selection] @@ -221,7 +221,7 @@ extension IR { if matchesType { typeInfo.entity.selectionTree.mergeIn( - selections: irTypeCaseEnclosingFragment.selections.direct.unsafelyUnwrapped.readOnlyView, + selections: irTypeCaseEnclosingFragment.selectionSet.selections.direct.unsafelyUnwrapped.readOnlyView, with: typeInfo ) } @@ -313,11 +313,11 @@ extension IR { return irSelectionSet } - private func buildConditionalSelectionSet( + private func buildInlineFragmentSpread( from selectionSet: CompilationResult.SelectionSet?, with scopeCondition: ScopeCondition, inParentTypePath enclosingTypeInfo: SelectionSet.TypeInfo - ) -> SelectionSet { + ) -> InlineFragmentSpread { let typePath = enclosingTypeInfo.scopePath.mutatingLast { $0.appending(scopeCondition) } @@ -334,14 +334,18 @@ extension IR { from: selectionSet ) } - return irSelectionSet - } - private func buildFragmentSpread( + return InlineFragmentSpread( + selectionSet: irSelectionSet, + isDeferred: scopeCondition.isDeferred + ) + } + + private func buildNamedFragmentSpread( fromFragment fragmentSpread: CompilationResult.FragmentSpread, with scopeCondition: ScopeCondition, spreadIntoParentWithTypePath parentTypeInfo: SelectionSet.TypeInfo - ) -> FragmentSpread { + ) -> NamedFragmentSpread { let fragment = ir.build(fragment: fragmentSpread.fragment) referencedFragments.append(fragment) referencedFragments.append(contentsOf: fragment.referencedFragments) @@ -357,14 +361,15 @@ extension IR { scopePath: scopePath ) - let fragmentSpread = FragmentSpread( + let fragmentSpread = NamedFragmentSpread( fragment: fragment, typeInfo: typeInfo, - inclusionConditions: AnyOf(scopeCondition.conditions) + inclusionConditions: AnyOf(scopeCondition.conditions), + isDeferred: scopeCondition.isDeferred ) entityStorage.mergeAllSelectionsIntoEntitySelectionTrees(from: fragmentSpread) - + return fragmentSpread } diff --git a/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift b/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift index 102f0299c..633712db8 100644 --- a/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift +++ b/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift @@ -1,6 +1,6 @@ extension IR { @dynamicMemberLookup - class SelectionSet: Equatable, CustomDebugStringConvertible { + class SelectionSet: Hashable, CustomDebugStringConvertible { class TypeInfo: Hashable, CustomDebugStringConvertible { /// The entity that the `selections` are being selected on. /// @@ -141,9 +141,15 @@ extension IR { } static func ==(lhs: IR.SelectionSet, rhs: IR.SelectionSet) -> Bool { - lhs.typeInfo.entity === rhs.typeInfo.entity && - lhs.typeInfo.scopePath == rhs.typeInfo.scopePath && - lhs.selections.direct == rhs.selections.direct + lhs.typeInfo === rhs.typeInfo && + lhs.selections.direct === rhs.selections.direct + } + + func hash(into hasher: inout Hasher) { + hasher.combine(typeInfo) + if let directSelections = selections.direct { + hasher.combine(ObjectIdentifier(directSelections)) + } } subscript(dynamicMember keyPath: KeyPath) -> T { diff --git a/Sources/ApolloCodegenLib/IR/IR.swift b/Sources/ApolloCodegenLib/IR/IR.swift index a019ea86c..b19724aae 100644 --- a/Sources/ApolloCodegenLib/IR/IR.swift +++ b/Sources/ApolloCodegenLib/IR/IR.swift @@ -57,6 +57,30 @@ class IR { } } + // TODO: Documentation for this to be completed in issue #3141 + enum IsDeferred: Hashable, ExpressibleByBooleanLiteral { + case value(Bool) + case `if`(_ variable: String) + + init(booleanLiteral value: BooleanLiteralType) { + switch value { + case true: + self = .value(true) + case false: + self = .value(false) + } + } + + var definitionDirectiveDescription: String { + switch self { + case .value(false): return "" + case .value(true): return " @defer" + case let .if(variable): + return " @defer(if: \(variable))" + } + } + } + /// Represents a concrete entity in an operation or fragment that fields are selected upon. /// /// Multiple `SelectionSet`s may select fields on the same `Entity`. All `SelectionSet`s that will @@ -238,12 +262,55 @@ class IR { } } - /// Represents a Fragment that has been "spread into" another SelectionSet using the + /// Represents an Inline Fragment that has been "spread into" another SelectionSet using the + /// spread operator (`...`). + class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { + /// The `SelectionSet` representing the inline fragment that has been "spread into" its + /// enclosing operation/fragment. + let selectionSet: SelectionSet + + let isDeferred: IsDeferred + + /// Indicates the location where the inline fragment has been "spread into" its enclosing + /// operation/fragment. + var typeInfo: SelectionSet.TypeInfo { selectionSet.typeInfo } + + var inclusionConditions: InclusionConditions? { selectionSet.inclusionConditions } + + init( + selectionSet: SelectionSet, + isDeferred: IsDeferred + ) { + self.selectionSet = selectionSet + self.isDeferred = isDeferred + } + + static func == (lhs: IR.InlineFragmentSpread, rhs: IR.InlineFragmentSpread) -> Bool { + lhs.selectionSet == rhs.selectionSet && + lhs.isDeferred == rhs.isDeferred + } + + func hash(into hasher: inout Hasher) { + hasher.combine(selectionSet) + hasher.combine(isDeferred) + } + + var debugDescription: String { + var string = typeInfo.parentType.debugDescription + if let conditions = typeInfo.inclusionConditions { + string += " \(conditions.debugDescription)" + } + string += isDeferred.definitionDirectiveDescription + return string + } + } + + /// Represents a Named Fragment that has been "spread into" another SelectionSet using the /// spread operator (`...`). /// - /// While a `NamedFragment` can be shared between operations, a `FragmentSpread` represents a + /// While a `NamedFragment` can be shared between operations, a `NamedFragmentSpread` represents a /// `NamedFragment` included in a specific operation. - class FragmentSpread: Hashable, CustomDebugStringConvertible { + class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { /// The `NamedFragment` that this fragment refers to. /// @@ -260,35 +327,42 @@ class IR { var inclusionConditions: AnyOf? + let isDeferred: IsDeferred + var definition: CompilationResult.FragmentDefinition { fragment.definition } init( fragment: NamedFragment, typeInfo: SelectionSet.TypeInfo, - inclusionConditions: AnyOf? + inclusionConditions: AnyOf?, + isDeferred: IsDeferred ) { self.fragment = fragment self.typeInfo = typeInfo self.inclusionConditions = inclusionConditions + self.isDeferred = isDeferred } - static func == (lhs: IR.FragmentSpread, rhs: IR.FragmentSpread) -> Bool { + static func == (lhs: IR.NamedFragmentSpread, rhs: IR.NamedFragmentSpread) -> Bool { lhs.fragment === rhs.fragment && lhs.typeInfo == rhs.typeInfo && - lhs.inclusionConditions == rhs.inclusionConditions + lhs.inclusionConditions == rhs.inclusionConditions && + lhs.isDeferred == rhs.isDeferred } func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(fragment)) hasher.combine(typeInfo) hasher.combine(inclusionConditions) + hasher.combine(isDeferred) } - + var debugDescription: String { var description = fragment.debugDescription if let inclusionConditions = inclusionConditions { description += " \(inclusionConditions.debugDescription)" } + description += isDeferred.definitionDirectiveDescription return description } } diff --git a/Sources/ApolloCodegenLib/IR/ScopeDescriptor.swift b/Sources/ApolloCodegenLib/IR/ScopeDescriptor.swift index 7a3861f6f..5ded5d89a 100644 --- a/Sources/ApolloCodegenLib/IR/ScopeDescriptor.swift +++ b/Sources/ApolloCodegenLib/IR/ScopeDescriptor.swift @@ -3,13 +3,22 @@ import OrderedCollections extension IR { + // TODO: Write tests that two inline fragments with same type and inclusion conditions, + // but different defer conditions don't merge together. + // To be done in issue #3141 struct ScopeCondition: Hashable, CustomDebugStringConvertible { let type: GraphQLCompositeType? let conditions: InclusionConditions? + let isDeferred: IsDeferred - init(type: GraphQLCompositeType? = nil, conditions: InclusionConditions? = nil) { + init( + type: GraphQLCompositeType? = nil, + conditions: InclusionConditions? = nil, + isDeferred: IsDeferred = false + ) { self.type = type self.conditions = conditions + self.isDeferred = isDeferred } var debugDescription: String { @@ -96,7 +105,10 @@ extension IR { ) -> ScopeDescriptor { let scope = Self.typeScope(addingType: type, to: nil, givenAllTypes: allTypes) return ScopeDescriptor( - typePath: LinkedList(.init(type: type, conditions: inclusionConditions)), + typePath: LinkedList(.init( + type: type, + conditions: inclusionConditions + )), type: type, matchingTypes: scope, matchingConditions: inclusionConditions, diff --git a/Sources/ApolloCodegenLib/IR/ScopedSelectionSetHashable.swift b/Sources/ApolloCodegenLib/IR/ScopedSelectionSetHashable.swift index a77d99706..791d45fdd 100644 --- a/Sources/ApolloCodegenLib/IR/ScopedSelectionSetHashable.swift +++ b/Sources/ApolloCodegenLib/IR/ScopedSelectionSetHashable.swift @@ -25,7 +25,7 @@ extension CompilationResult.FragmentSpread: ScopedSelectionSetHashable { } } -extension IR.FragmentSpread: ScopedSelectionSetHashable { +extension IR.NamedFragmentSpread: ScopedSelectionSetHashable { var hashForSelectionSetScope: String { fragment.definition.name } diff --git a/Sources/ApolloCodegenLib/IR/SortedSelections.swift b/Sources/ApolloCodegenLib/IR/SortedSelections.swift index 07416d938..60d9773fb 100644 --- a/Sources/ApolloCodegenLib/IR/SortedSelections.swift +++ b/Sources/ApolloCodegenLib/IR/SortedSelections.swift @@ -6,35 +6,35 @@ extension IR { class DirectSelections: Equatable, CustomDebugStringConvertible { fileprivate(set) var fields: OrderedDictionary = [:] - fileprivate(set) var inlineFragments: OrderedDictionary = [:] - fileprivate(set) var fragments: OrderedDictionary = [:] + fileprivate(set) var inlineFragments: OrderedDictionary = [:] + fileprivate(set) var namedFragments: OrderedDictionary = [:] init() {} init( fields: [Field] = [], - conditionalSelectionSets: [SelectionSet] = [], - fragments: [FragmentSpread] = [] + inlineFragments: [InlineFragmentSpread] = [], + namedFragments: [NamedFragmentSpread] = [] ) { mergeIn(fields) - mergeIn(conditionalSelectionSets) - mergeIn(fragments) + mergeIn(inlineFragments) + mergeIn(namedFragments) } init( fields: OrderedDictionary = [:], - conditionalSelectionSets: OrderedDictionary = [:], - fragments: OrderedDictionary = [:] + inlineFragments: OrderedDictionary = [:], + namedFragments: OrderedDictionary = [:] ) { mergeIn(fields.values) - mergeIn(conditionalSelectionSets.values) - mergeIn(fragments.values) + mergeIn(inlineFragments.values) + mergeIn(namedFragments.values) } func mergeIn(_ selections: DirectSelections) { mergeIn(selections.fields.values) mergeIn(selections.inlineFragments.values) - mergeIn(selections.fragments.values) + mergeIn(selections.namedFragments.values) } func mergeIn(_ field: Field) { @@ -108,62 +108,66 @@ extension IR { }, selections: newField.selectionSet.selections.direct.unsafelyUnwrapped ) - wrapperField.selectionSet.selections.direct?.mergeIn(newFieldSelectionSet) + let newFieldInlineFragment = InlineFragmentSpread( + selectionSet: newFieldSelectionSet, + isDeferred: false + ) + wrapperField.selectionSet.selections.direct?.mergeIn(newFieldInlineFragment) } else { wrapperField.selectionSet.selections.direct?.mergeIn(newField.selectionSet.selections.direct.unsafelyUnwrapped) } } - func mergeIn(_ conditionalSelectionSet: SelectionSet) { - let scopeCondition = conditionalSelectionSet.scope.scopePath.last.value + func mergeIn(_ fragment: InlineFragmentSpread) { + let scopeCondition = fragment.selectionSet.scope.scopePath.last.value - if let existingTypeCase = inlineFragments[scopeCondition] { + if let existingTypeCase = inlineFragments[scopeCondition]?.selectionSet { existingTypeCase.selections.direct! - .mergeIn(conditionalSelectionSet.selections.direct!) + .mergeIn(fragment.selectionSet.selections.direct!) } else { - inlineFragments[scopeCondition] = conditionalSelectionSet + inlineFragments[scopeCondition] = fragment } } - func mergeIn(_ fragment: FragmentSpread) { - if let existingFragment = fragments[fragment.hashForSelectionSetScope] { + func mergeIn(_ fragment: NamedFragmentSpread) { + if let existingFragment = namedFragments[fragment.hashForSelectionSetScope] { existingFragment.inclusionConditions = (existingFragment.inclusionConditions || fragment.inclusionConditions) return } - fragments[fragment.hashForSelectionSetScope] = fragment + namedFragments[fragment.hashForSelectionSetScope] = fragment } func mergeIn(_ fields: T) where T.Element == Field { fields.forEach { mergeIn($0) } } - func mergeIn(_ conditionalSelectionSets: T) where T.Element == SelectionSet { - conditionalSelectionSets.forEach { mergeIn($0) } + func mergeIn(_ inlineFragments: T) where T.Element == InlineFragmentSpread { + inlineFragments.forEach { mergeIn($0) } } - func mergeIn(_ fragments: T) where T.Element == FragmentSpread { + func mergeIn(_ fragments: T) where T.Element == NamedFragmentSpread { fragments.forEach { mergeIn($0) } } var isEmpty: Bool { - fields.isEmpty && inlineFragments.isEmpty && fragments.isEmpty + fields.isEmpty && inlineFragments.isEmpty && namedFragments.isEmpty } static func == (lhs: DirectSelections, rhs: DirectSelections) -> Bool { lhs.fields == rhs.fields && lhs.inlineFragments == rhs.inlineFragments && - lhs.fragments == rhs.fragments + lhs.namedFragments == rhs.namedFragments } var debugDescription: String { """ Fields: \(fields.values.elements) - InlineFragments: \(inlineFragments.values.elements.map(\.inlineFragmentDebugDescription)) - Fragments: \(fragments.values.elements.map(\.debugDescription)) + InlineFragments: \(inlineFragments.values.elements.map(\.debugDescription)) + Fragments: \(namedFragments.values.elements.map(\.debugDescription)) """ } @@ -175,8 +179,8 @@ extension IR { fileprivate let value: DirectSelections var fields: OrderedDictionary { value.fields } - var inlineFragments: OrderedDictionary { value.inlineFragments } - var fragments: OrderedDictionary { value.fragments } + var inlineFragments: OrderedDictionary { value.inlineFragments } + var namedFragments: OrderedDictionary { value.namedFragments } var isEmpty: Bool { value.isEmpty } } @@ -219,16 +223,16 @@ extension IR { } } - for selection in directSelections.fragments { + for selection in directSelections.namedFragments { if let condition = selection.value.inclusionConditions { inclusionConditionGroups.updateValue( forKey: condition, default: .init(value: DirectSelections())) { selections in - selections.value.fragments[selection.key] = selection.value + selections.value.namedFragments[selection.key] = selection.value } } else { - unconditionalSelections.value.fragments[selection.key] = selection.value + unconditionalSelections.value.namedFragments[selection.key] = selection.value } } } @@ -263,8 +267,8 @@ extension IR { fileprivate(set) var mergedSources: MergedSources = [] fileprivate(set) var fields: OrderedDictionary = [:] - fileprivate(set) var inlineFragments: OrderedDictionary = [:] - fileprivate(set) var fragments: OrderedDictionary = [:] + fileprivate(set) var inlineFragments: OrderedDictionary = [:] + fileprivate(set) var namedFragments: OrderedDictionary = [:] init( directSelections: DirectSelections.ReadOnly?, @@ -278,7 +282,7 @@ extension IR { @IsEverTrue var didMergeAnySelections: Bool selections.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } - selections.fragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } + selections.namedFragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } if didMergeAnySelections { mergedSources.append(source) @@ -317,14 +321,14 @@ extension IR { ) } - private func mergeIn(_ fragment: IR.FragmentSpread) -> Bool { + private func mergeIn(_ fragment: IR.NamedFragmentSpread) -> Bool { let keyInScope = fragment.hashForSelectionSetScope if let directSelections = directSelections, - directSelections.fragments.keys.contains(keyInScope) { + directSelections.namedFragments.keys.contains(keyInScope) { return false } - fragments[keyInScope] = fragment + namedFragments[keyInScope] = fragment return true } @@ -345,31 +349,34 @@ extension IR { guard !inlineFragments.keys.contains(condition) else { return } - let selectionSet = IR.SelectionSet( - entity: self.typeInfo.entity, - scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) }, - mergedSelectionsOnly: true + let inlineFragment = IR.InlineFragmentSpread( + selectionSet: .init( + entity: self.typeInfo.entity, + scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) }, + mergedSelectionsOnly: true + ), + isDeferred: condition.isDeferred ) - inlineFragments[condition] = selectionSet + inlineFragments[condition] = inlineFragment } var isEmpty: Bool { - fields.isEmpty && inlineFragments.isEmpty && fragments.isEmpty + fields.isEmpty && inlineFragments.isEmpty && namedFragments.isEmpty } static func == (lhs: MergedSelections, rhs: MergedSelections) -> Bool { lhs.mergedSources == rhs.mergedSources && lhs.fields == rhs.fields && lhs.inlineFragments == rhs.inlineFragments && - lhs.fragments == rhs.fragments + lhs.namedFragments == rhs.namedFragments } var debugDescription: String { """ Merged Sources: \(mergedSources) Fields: \(fields.values.elements) - InlineFragments: \(inlineFragments.values.elements.map(\.inlineFragmentDebugDescription)) - Fragments: \(fragments.values.elements.map(\.debugDescription)) + InlineFragments: \(inlineFragments.values.elements.map(\.debugDescription)) + NamedFragments: \(namedFragments.values.elements.map(\.debugDescription)) """ } @@ -377,16 +384,6 @@ extension IR { } -fileprivate extension IR.SelectionSet { - var inlineFragmentDebugDescription: String { - var string = typeInfo.parentType.debugDescription - if let conditions = typeInfo.inclusionConditions { - string += " \(conditions.debugDescription)" - } - return string - } -} - extension IR.MergedSelections.MergedSource: CustomDebugStringConvertible { var debugDescription: String { typeInfo.debugDescription + ", fragment: \(fragment?.debugDescription ?? "nil")" diff --git a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index 435f44137..756f955b3 100644 --- a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -204,8 +204,8 @@ struct SelectionSetTemplate { _ deprecatedArguments: inout [DeprecatedArgument]? ) -> [TemplateString] { selections.fields.values.map { FieldSelectionTemplate($0, &deprecatedArguments) } + - selections.inlineFragments.values.map { InlineFragmentSelectionTemplate($0) } + - selections.fragments.values.map { FragmentSelectionTemplate($0) } + selections.inlineFragments.values.map { InlineFragmentSelectionTemplate($0.selectionSet) } + + selections.namedFragments.values.map { FragmentSelectionTemplate($0) } } private func renderedConditionalSelectionGroup( @@ -283,7 +283,7 @@ struct SelectionSetTemplate { """ } - private func FragmentSelectionTemplate(_ fragment: IR.FragmentSpread) -> TemplateString { + private func FragmentSelectionTemplate(_ fragment: IR.NamedFragmentSpread) -> TemplateString { """ .fragment(\(fragment.definition.name.asFragmentName).self) """ @@ -329,9 +329,9 @@ struct SelectionSetTemplate { ) -> TemplateString { """ \(ifLet: selections.direct?.inlineFragments.values, { - "\($0.map { InlineFragmentAccessorTemplate($0) }, separator: "\n")" + "\($0.map { InlineFragmentAccessorTemplate($0.selectionSet) }, separator: "\n")" }) - \(selections.merged.inlineFragments.values.map { InlineFragmentAccessorTemplate($0) }, separator: "\n") + \(selections.merged.inlineFragments.values.map { InlineFragmentAccessorTemplate($0.selectionSet) }, separator: "\n") """ } @@ -355,8 +355,8 @@ struct SelectionSetTemplate { _ selections: IR.SelectionSet.Selections, in scope: IR.ScopeDescriptor ) -> TemplateString { - guard !(selections.direct?.fragments.isEmpty ?? true) || - !selections.merged.fragments.isEmpty else { + guard !(selections.direct?.namedFragments.isEmpty ?? true) || + !selections.merged.namedFragments.isEmpty else { return "" } @@ -364,18 +364,18 @@ struct SelectionSetTemplate { \(renderAccessControl())struct Fragments: FragmentContainer { \(DataFieldAndInitializerTemplate()) - \(ifLet: selections.direct?.fragments.values, { - "\($0.map { FragmentAccessorTemplate($0, in: scope) }, separator: "\n")" + \(ifLet: selections.direct?.namedFragments.values, { + "\($0.map { NamedFragmentAccessorTemplate($0, in: scope) }, separator: "\n")" }) - \(selections.merged.fragments.values.map { - FragmentAccessorTemplate($0, in: scope) + \(selections.merged.namedFragments.values.map { + NamedFragmentAccessorTemplate($0, in: scope) }, separator: "\n") } """ } - private func FragmentAccessorTemplate( - _ fragment: IR.FragmentSpread, + private func NamedFragmentAccessorTemplate( + _ fragment: IR.NamedFragmentSpread, in scope: IR.ScopeDescriptor ) -> TemplateString { let name = fragment.definition.name @@ -532,9 +532,9 @@ struct SelectionSetTemplate { private func ChildTypeCaseSelectionSets(_ selections: IR.SelectionSet.Selections) -> TemplateString { """ \(ifLet: selections.direct?.inlineFragments.values, { - "\($0.map { render(inlineFragment: $0) }, separator: "\n\n")" + "\($0.map { render(inlineFragment: $0.selectionSet) }, separator: "\n\n")" }) - \(selections.merged.inlineFragments.values.map { render(inlineFragment: $0) }, separator: "\n\n") + \(selections.merged.inlineFragments.values.map { render(inlineFragment: $0.selectionSet) }, separator: "\n\n") """ } @@ -950,8 +950,8 @@ extension IR.SelectionSet.Selections { SelectionsIterator(direct: direct?.fields, merged: merged.fields) } - fileprivate func makeFragmentIterator() -> SelectionsIterator { - SelectionsIterator(direct: direct?.fragments, merged: merged.fragments) + fileprivate func makeFragmentIterator() -> SelectionsIterator { + SelectionsIterator(direct: direct?.namedFragments, merged: merged.namedFragments) } fileprivate struct SelectionsIterator: IteratorProtocol { diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift index 44a44569d..59ec5b58d 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift @@ -79,11 +79,11 @@ extension IR.DirectSelections: ScopeConditionalSubscriptAccessing { } public subscript(conditions: IR.ScopeCondition) -> IR.SelectionSet? { - inlineFragments[conditions] + inlineFragments[conditions]?.selectionSet } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { - fragments[fragment] + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { + namedFragments[fragment] } } @@ -93,11 +93,11 @@ extension IR.MergedSelections: ScopeConditionalSubscriptAccessing { } public subscript(conditions: IR.ScopeCondition) -> IR.SelectionSet? { - inlineFragments[conditions] + inlineFragments[conditions]?.selectionSet } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { - fragments[fragment] + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { + namedFragments[fragment] } } @@ -106,8 +106,8 @@ extension IR.EntityTreeScopeSelections { fields[field] } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { - fragments[fragment] + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { + namedFragments[fragment] } } @@ -121,7 +121,7 @@ extension IR.Field: ScopeConditionalSubscriptAccessing { return selectionSet?[conditions] } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { return selectionSet?[fragment: fragment] } @@ -140,7 +140,7 @@ extension IR.SelectionSet: ScopeConditionalSubscriptAccessing { selections[conditions] } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { selections[fragment: fragment] } } @@ -151,11 +151,12 @@ extension IR.SelectionSet.Selections: ScopeConditionalSubscriptAccessing { } public subscript(conditions: IR.ScopeCondition) -> IR.SelectionSet? { - return direct?.inlineFragments[conditions] ?? merged.inlineFragments[conditions] + return direct?.inlineFragments[conditions]?.selectionSet ?? + merged.inlineFragments[conditions]?.selectionSet } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { - direct?.fragments[fragment] ?? merged.fragments[fragment] + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { + direct?.namedFragments[fragment] ?? merged.namedFragments[fragment] } } @@ -168,7 +169,7 @@ extension IR.Operation: ScopeConditionalSubscriptAccessing { rootField[conditions] } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { rootField[fragment: fragment] } } @@ -182,7 +183,7 @@ extension IR.NamedFragment: ScopeConditionalSubscriptAccessing { return rootField.selectionSet[conditions] } - public subscript(fragment fragment: String) -> IR.FragmentSpread? { + public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { rootField.selectionSet[fragment: fragment] } } diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift b/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift index 5acd04701..edd86e014 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift @@ -27,7 +27,7 @@ extension IR.MergedSelections.MergedSource { } public static func mock( - _ fragment: IR.FragmentSpread?, + _ fragment: IR.NamedFragmentSpread?, file: StaticString = #file, line: UInt = #line ) throws -> Self { @@ -40,7 +40,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( for field: IR.Field?, - from fragment: IR.FragmentSpread?, + from fragment: IR.NamedFragmentSpread?, file: StaticString = #file, line: UInt = #line ) throws -> Self { diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index ed6366c77..230be54c5 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -127,7 +127,7 @@ class IRRootFieldBuilderTests: XCTestCase { let child = allAnimals?[as: "Bird"] expect(child?.parentType).to(equal(Object_Bird)) - expect(child?.selections.direct?.fragments.values).to(shallowlyMatch([Fragment_BirdDetails])) + expect(child?.selections.direct?.namedFragments.values).to(shallowlyMatch([Fragment_BirdDetails])) } func test__children__isObjectType_initWithNamedFragmentOnLessSpecificMatchingType_hasNoChildTypeCase() throws { @@ -245,8 +245,8 @@ class IRRootFieldBuilderTests: XCTestCase { let child = rocks?[as: "Animal"] expect(child?.parentType).to(equal(Interface_Animal)) - expect(child?.selections.direct?.fragments.count).to(equal(1)) - expect(child?.selections.direct?.fragments.values[0].definition).to(equal(Fragment_AnimalDetails)) + expect(child?.selections.direct?.namedFragments.count).to(equal(1)) + expect(child?.selections.direct?.namedFragments.values[0].definition).to(equal(Fragment_AnimalDetails)) } // MARK: Children Computation - Union Type @@ -370,10 +370,21 @@ class IRRootFieldBuilderTests: XCTestCase { // when try buildSubjectRootField() + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Object_B = try XCTUnwrap(schema[object: "B"]) + let bField = subject[field: "bField"] + let expected = SelectionSetMatcher( + parentType: Object_B, + directSelections: [ + .field("A", type: .nonNull(.scalar(Scalar_String))), + .field("B", type: .nonNull(.scalar(Scalar_String))), + ] + ) + // then - expect(bField?.selectionSet?.selections.direct?.inlineFragments).to(beEmpty()) + expect(bField?.selectionSet).to(shallowlyMatch(expected)) } func test__children__givenInlineFragment_onNonMatchingType_doesNotMergeTypeCaseIn_hasChildTypeCase() throws { diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index e04458c72..5c0a9cef6 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -6,12 +6,12 @@ import ApolloInternalTestHelpers protocol SelectionShallowMatchable { typealias Field = IR.Field - typealias TypeCase = IR.SelectionSet - typealias Fragment = IR.FragmentSpread + typealias InlineFragment = IR.InlineFragmentSpread + typealias NamedFragment = IR.NamedFragmentSpread var fields: OrderedDictionary { get } - var inlineFragments: OrderedDictionary { get } - var fragments: OrderedDictionary { get } + var inlineFragments: OrderedDictionary { get } + var namedFragments: OrderedDictionary { get } var isEmpty: Bool { get } } @@ -20,7 +20,7 @@ extension IR.DirectSelections: SelectionShallowMatchable { } extension IR.DirectSelections.ReadOnly: SelectionShallowMatchable {} extension IR.MergedSelections: SelectionShallowMatchable { } extension IR.EntityTreeScopeSelections: SelectionShallowMatchable { - var inlineFragments: OrderedDictionary { [:] } + var inlineFragments: OrderedDictionary { [:] } } typealias SelectionMatcherTuple = (fields: [ShallowFieldMatcher], @@ -44,8 +44,8 @@ func shallowlyMatch( ) -> Predicate { return satisfyAllOf([ shallowlyMatch(expectedValue.fields).mappingActualTo { $0?.fields.values }, - shallowlyMatch(expectedValue.typeCases).mappingActualTo { $0?.inlineFragments.values }, - shallowlyMatch(expectedValue.fragments).mappingActualTo { $0?.fragments.values } + shallowlyMatch(expectedValue.typeCases).mappingActualTo { $0?.inlineFragments.values.map(\.selectionSet) }, + shallowlyMatch(expectedValue.fragments).mappingActualTo { $0?.namedFragments.values } ]) } @@ -468,7 +468,7 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti public func shallowlyMatch( _ expectedValue: [ShallowFragmentSpreadMatcher] -) -> Predicate where T.Element == IR.FragmentSpread { +) -> Predicate where T.Element == IR.NamedFragmentSpread { return Predicate.define { actual in return shallowlyMatch(expected: expectedValue, actual: try actual.evaluate()) } @@ -476,7 +476,7 @@ public func shallowlyMatch( public func shallowlyMatch( _ expectedValue: [CompilationResult.FragmentDefinition] -) -> Predicate where T.Element == IR.FragmentSpread { +) -> Predicate where T.Element == IR.NamedFragmentSpread { return Predicate.define { actual in return shallowlyMatch(expected: expectedValue.map { .mock($0) }, actual: try actual.evaluate()) } @@ -485,7 +485,7 @@ public func shallowlyMatch( fileprivate func shallowlyMatch( expected: [ShallowFragmentSpreadMatcher], actual: T? -) -> PredicateResult where T.Element == IR.FragmentSpread { +) -> PredicateResult where T.Element == IR.NamedFragmentSpread { let message: ExpectationMessage = .expectedActualValueTo("have fragments equal to \(expected)") guard let actual = actual, expected.count == actual.count else { @@ -506,7 +506,7 @@ fileprivate func shallowlyMatch( return PredicateResult(status: .matches, message: message) } -fileprivate func shallowlyMatch(expected: ShallowFragmentSpreadMatcher, actual: IR.FragmentSpread) -> Bool { +fileprivate func shallowlyMatch(expected: ShallowFragmentSpreadMatcher, actual: IR.NamedFragmentSpread) -> Bool { return expected.name == actual.fragment.name && expected.type == actual.fragment.type && expected.inclusionConditions == actual.inclusionConditions