diff --git a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index 71e841f6..d8d0d0b4 100644 --- a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -121,7 +121,7 @@ struct SelectionSetTemplate { \(SelectionSetNameDocumentation(inlineFragment)) \(renderAccessControl())\ struct \(inlineFragment.renderedTypeName): \(SelectionSetType(asInlineFragment: true))\ - \(if: inlineFragment.isCompositeSelectionSet, ", \(config.ApolloAPITargetName).CompositeInlineFragment")\ + \(if: inlineFragment.isCompositeInlineFragment, ", \(config.ApolloAPITargetName).CompositeInlineFragment")\ \(if: inlineFragment.isDeferred, ", \(config.ApolloAPITargetName).Deferrable")\ { \(BodyTemplate(context)) @@ -274,18 +274,25 @@ struct SelectionSetTemplate { var deprecatedArguments: [DeprecatedArgument]? = config.options.warningsOnDeprecatedUsage == .include ? [] : nil - let selectionsTemplate = TemplateString( - """ - \(renderAccessControl())\ - static var __selections: [\(config.ApolloAPITargetName).Selection] { [ - \(if: shouldIncludeTypenameSelection(for: scope), ".field(\"__typename\", String.self),") - \(renderedSelections(groupedSelections.unconditionalSelections, &deprecatedArguments), terminator: ",") - \(groupedSelections.inclusionConditionGroups.map { - renderedConditionalSelectionGroup($0, $1, in: scope, &deprecatedArguments) - }, terminator: ",") - ] } - """ - ) + let shouldIncludeTypenameSelection = shouldIncludeTypenameSelection(for: scope) + let selectionsTemplate: TemplateString + + if !groupedSelections.isEmpty || shouldIncludeTypenameSelection { + selectionsTemplate = TemplateString(""" + \(renderAccessControl())\ + static var __selections: [\(config.ApolloAPITargetName).Selection] { [ + \(if: shouldIncludeTypenameSelection, ".field(\"__typename\", String.self),") + \(renderedSelections(groupedSelections.unconditionalSelections, &deprecatedArguments), terminator: ",") + \(groupedSelections.inclusionConditionGroups.map { + renderedConditionalSelectionGroup($0, $1, in: scope, &deprecatedArguments) + }, terminator: ",") + ] } + """ + ) + } else { + selectionsTemplate = "" + } + return """ \(if: deprecatedArguments != nil && !deprecatedArguments.unsafelyUnwrapped.isEmpty, """ \(deprecatedArguments.unsafelyUnwrapped.map { """ @@ -752,12 +759,8 @@ private class SelectionSetNameCache { extension IR.ComputedSelectionSet { - fileprivate var isCompositeSelectionSet: Bool { - return direct?.isEmpty ?? true - } - fileprivate var isCompositeInlineFragment: Bool { - return !self.isEntityRoot && isCompositeSelectionSet + return !self.isEntityRoot && !self.isUserDefined && (direct?.isEmpty ?? true) } fileprivate var shouldBeRendered: Bool { diff --git a/Sources/IR/IR+ComputedSelectionSet.swift b/Sources/IR/IR+ComputedSelectionSet.swift index 1af61f50..d885dfc0 100644 --- a/Sources/IR/IR+ComputedSelectionSet.swift +++ b/Sources/IR/IR+ComputedSelectionSet.swift @@ -104,7 +104,8 @@ extension ComputedSelectionSet { private func createShallowlyMergedNestedEntityField(from field: EntityField) -> EntityField { let typeInfo = SelectionSet.TypeInfo( entity: entityStorage.entity(for: field.underlyingField, on: typeInfo.entity), - scopePath: self.typeInfo.scopePath.appending(field.selectionSet.typeInfo.scope) + scopePath: self.typeInfo.scopePath.appending(field.selectionSet.typeInfo.scope), + isUserDefined: false ) let newSelectionSet = SelectionSet( @@ -149,7 +150,8 @@ extension ComputedSelectionSet { let typeInfo = SelectionSet.TypeInfo( entity: self.typeInfo.entity, - scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) } + scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) }, + isUserDefined: false ) let selectionSet = SelectionSet( diff --git a/Sources/IR/IR+DirectSelections.swift b/Sources/IR/IR+DirectSelections.swift index 6bdfbc56..7e408141 100644 --- a/Sources/IR/IR+DirectSelections.swift +++ b/Sources/IR/IR+DirectSelections.swift @@ -85,7 +85,8 @@ public class DirectSelections: Equatable, CustomDebugStringConvertible { let typeInfo = SelectionSet.TypeInfo( entity: existingField.entity, - scopePath: wrapperScope + scopePath: wrapperScope, + isUserDefined: true ) let selectionSet = SelectionSet( @@ -111,7 +112,8 @@ public class DirectSelections: Equatable, CustomDebugStringConvertible { entity: newField.entity, scopePath: wrapperField.selectionSet.scopePath.mutatingLast { $0.appending(newFieldConditions) - } + }, + isUserDefined: true ) let newFieldSelectionSet = SelectionSet( @@ -209,6 +211,10 @@ public class DirectSelections: Equatable, CustomDebugStringConvertible { public private(set) var inclusionConditionGroups: OrderedDictionary, DirectSelections.ReadOnly> = [:] + public var isEmpty: Bool { + unconditionalSelections.isEmpty && inclusionConditionGroups.isEmpty + } + init(_ directSelections: DirectSelections.ReadOnly) { for selection in directSelections.fields { if let condition = selection.value.inclusionConditions { diff --git a/Sources/IR/IR+RootFieldBuilder.swift b/Sources/IR/IR+RootFieldBuilder.swift index a8c843eb..967c46b4 100644 --- a/Sources/IR/IR+RootFieldBuilder.swift +++ b/Sources/IR/IR+RootFieldBuilder.swift @@ -74,7 +74,8 @@ class RootFieldBuilder { ) async -> SelectionSet { let typeInfo = SelectionSet.TypeInfo( entity: entity, - scopePath: scopePath + scopePath: scopePath, + isUserDefined: true ) var directSelections: DirectSelections? = nil @@ -420,7 +421,8 @@ class RootFieldBuilder { let typeInfo = SelectionSet.TypeInfo( entity: parentTypeInfo.entity, - scopePath: scopePath + scopePath: scopePath, + isUserDefined: true ) let fragmentSpread = NamedFragmentSpread( diff --git a/Sources/IR/IR+SelectionSet.swift b/Sources/IR/IR+SelectionSet.swift index be467cd4..673d55bd 100644 --- a/Sources/IR/IR+SelectionSet.swift +++ b/Sources/IR/IR+SelectionSet.swift @@ -15,6 +15,13 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { /// The selection set's `scope` is the last element in the list. public let scopePath: LinkedList + /// Indicates if the `SelectionSet` was created directly due to a selection set in the user defined `.graphql` definition file. + /// + /// If `false`, the selection set was artificially created by the IR. Currently, the only reason for this is a `CompositeInlineFragment` created during calculation of merged selections for field merging. + public var isUserDefined: Bool + + // MARK: - Computed Properties + /// Describes all of the types and inclusion conditions the selection set matches. /// Derived from all the selection set's parents. public var scope: ScopeDescriptor { scopePath.last.value } @@ -28,6 +35,7 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { public var deferCondition: CompilationResult.DeferCondition? { scope.scopePath.last.value.deferCondition } + public var isDeferred: Bool { deferCondition != nil } /// Indicates if the `SelectionSet` represents a root selection set. @@ -38,10 +46,12 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { init( entity: Entity, - scopePath: LinkedList + scopePath: LinkedList, + isUserDefined: Bool ) { self.entity = entity self.scopePath = scopePath + self.isUserDefined = isUserDefined } public static func == (lhs: TypeInfo, rhs: TypeInfo) -> Bool {