Skip to content

polish: use closure instead of CollectFieldsContext #4361

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

Open
wants to merge 1 commit into
base: next
Choose a base branch
from
Open
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
472 changes: 210 additions & 262 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
@@ -62,17 +62,6 @@ export interface FragmentDetails {
variableSignatures?: ObjMap<GraphQLVariableSignature> | undefined;
}

interface CollectFieldsContext {
schema: GraphQLSchema;
fragments: ObjMap<FragmentDetails>;
variableValues: VariableValues;
runtimeType: GraphQLObjectType;
visitedFragmentNames: Set<string>;
hideSuggestions: boolean;
forbiddenDirectiveInstances: Array<DirectiveNode>;
forbidSkipAndInclude: boolean;
}

/**
* Given a selectionSet, collects all of the fields and returns them.
*
@@ -98,22 +87,26 @@ export function collectFields(
} {
const groupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];
const context: CollectFieldsContext = {
const forbiddenDirectiveInstances: Array<DirectiveNode> = [];

const selectionSetVisitor = buildSelectionSetVisitor(
schema,
fragments,
variableValues,
runtimeType,
visitedFragmentNames: new Set(),
hideSuggestions,
forbiddenDirectiveInstances: [],
forbidSkipAndInclude,
};
groupedFieldSet,
newDeferUsages,
forbiddenDirectiveInstances,
);

selectionSetVisitor(selectionSet);

collectFieldsImpl(context, selectionSet, groupedFieldSet, newDeferUsages);
return {
groupedFieldSet,
newDeferUsages,
forbiddenDirectiveInstances: context.forbiddenDirectiveInstances,
forbiddenDirectiveInstances,
};
}

@@ -139,31 +132,26 @@ export function collectSubfields(
groupedFieldSet: GroupedFieldSet;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const context: CollectFieldsContext = {
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];

const selectionSetVisitor = buildSelectionSetVisitor(
schema,
fragments,
variableValues,
runtimeType: returnType,
visitedFragmentNames: new Set(),
returnType,
hideSuggestions,
forbiddenDirectiveInstances: [],
forbidSkipAndInclude: false,
};
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];
false,
subGroupedFieldSet,
newDeferUsages,
[],
);

for (const fieldDetail of fieldDetailsList) {
const selectionSet = fieldDetail.node.selectionSet;
if (selectionSet) {
const { deferUsage, fragmentVariableValues } = fieldDetail;
collectFieldsImpl(
context,
selectionSet,
subGroupedFieldSet,
newDeferUsages,
deferUsage,
fragmentVariableValues,
);
selectionSetVisitor(selectionSet, deferUsage, fragmentVariableValues);
}
}

@@ -174,259 +162,219 @@ export function collectSubfields(
}

// eslint-disable-next-line @typescript-eslint/max-params
function collectFieldsImpl(
context: CollectFieldsContext,
selectionSet: SelectionSetNode,
function buildSelectionSetVisitor(
schema: GraphQLSchema,
fragments: ObjMap<FragmentDetails>,
variableValues: VariableValues,
runtimeType: GraphQLObjectType,
hideSuggestions: boolean,
forbidSkipAndInclude: boolean,
groupedFieldSet: AccumulatorMap<string, FieldDetails>,
newDeferUsages: Array<DeferUsage>,
forbiddenDirectiveInstances: Array<DirectiveNode>,
): (
node: SelectionSetNode,
deferUsage?: DeferUsage,
fragmentVariableValues?: FragmentVariableValues,
): void {
const {
schema,
fragments,
variableValues,
runtimeType,
visitedFragmentNames,
hideSuggestions,
} = context;

for (const selection of selectionSet.selections) {
switch (selection.kind) {
case Kind.FIELD: {
if (
!shouldIncludeNode(
context,
selection,
variableValues,
) => void {
const visitedFragmentNames = new Set<string>();

function selectionSetVisitor(
selectionSet: SelectionSetNode,
deferUsage?: DeferUsage,
fragmentVariableValues?: FragmentVariableValues,
): void {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case Kind.FIELD: {
if (!shouldIncludeNode(selection)) {
continue;
}
groupedFieldSet.add(getFieldEntryKey(selection), {
node: selection,
deferUsage,
fragmentVariableValues,
)
) {
continue;
});
break;
}
groupedFieldSet.add(getFieldEntryKey(selection), {
node: selection,
deferUsage,
fragmentVariableValues,
});
break;
}
case Kind.INLINE_FRAGMENT: {
if (
!shouldIncludeNode(
context,
selection,
variableValues,
fragmentVariableValues,
) ||
!doesFragmentConditionMatch(schema, selection, runtimeType)
) {
continue;
case Kind.INLINE_FRAGMENT: {
if (
!shouldIncludeNode(selection) ||
!doesFragmentConditionMatch(selection)
) {
continue;
}

const newDeferUsage = getDeferUsage(selection);

if (!newDeferUsage) {
selectionSetVisitor(
selection.selectionSet,
deferUsage,
fragmentVariableValues,
);
} else {
newDeferUsages.push(newDeferUsage);
selectionSetVisitor(
selection.selectionSet,
newDeferUsage,
fragmentVariableValues,
);
}
break;
}

const newDeferUsage = getDeferUsage(
variableValues,
fragmentVariableValues,
selection,
deferUsage,
);

if (!newDeferUsage) {
collectFieldsImpl(
context,
selection.selectionSet,
groupedFieldSet,
newDeferUsages,
deferUsage,
fragmentVariableValues,
);
} else {
newDeferUsages.push(newDeferUsage);
collectFieldsImpl(
context,
selection.selectionSet,
groupedFieldSet,
newDeferUsages,
newDeferUsage,
fragmentVariableValues,
);
case Kind.FRAGMENT_SPREAD: {
const fragName = selection.name.value;

if (
visitedFragmentNames.has(fragName) ||
!shouldIncludeNode(selection)
) {
continue;
}

const fragment = fragments[fragName];
if (
fragment == null ||
!doesFragmentConditionMatch(fragment.definition)
) {
continue;
}

const newDeferUsage = getDeferUsage(selection);

const fragmentVariableSignatures = fragment.variableSignatures;
let newFragmentVariableValues: FragmentVariableValues | undefined;
if (fragmentVariableSignatures) {
newFragmentVariableValues = getFragmentVariableValues(
selection,
fragmentVariableSignatures,
variableValues,
fragmentVariableValues,
hideSuggestions,
);
}

if (!newDeferUsage) {
visitedFragmentNames.add(fragName);
selectionSetVisitor(
fragment.definition.selectionSet,
deferUsage,
newFragmentVariableValues,
);
} else {
newDeferUsages.push(newDeferUsage);
selectionSetVisitor(
fragment.definition.selectionSet,
newDeferUsage,
newFragmentVariableValues,
);
}
break;
}
}
}

/**
* Returns an object containing the `@defer` arguments if a field should be
* deferred based on the experimental flag, defer directive present and
* not disabled by the "if" argument.
*/
function getDeferUsage(
node: FragmentSpreadNode | InlineFragmentNode,
): DeferUsage | undefined {
const defer = getDirectiveValues(
GraphQLDeferDirective,
node,
variableValues,
fragmentVariableValues,
);

if (!defer) {
return;
}

break;
if (defer.if === false) {
return;
}
case Kind.FRAGMENT_SPREAD: {
const fragName = selection.name.value;

if (
visitedFragmentNames.has(fragName) ||
!shouldIncludeNode(
context,
selection,

return {
label: typeof defer.label === 'string' ? defer.label : undefined,
parentDeferUsage: deferUsage,
};
}

/**
* Determines if a field should be included based on the `@include` and `@skip`
* directives, where `@skip` has higher precedence than `@include`.
*/
function shouldIncludeNode(
node: FragmentSpreadNode | FieldNode | InlineFragmentNode,
): boolean {
const skipDirectiveNode = node.directives?.find(
(directive) => directive.name.value === GraphQLSkipDirective.name,
);
if (skipDirectiveNode && forbidSkipAndInclude) {
forbiddenDirectiveInstances.push(skipDirectiveNode);
return false;
}
const skip = skipDirectiveNode
? getArgumentValues(
GraphQLSkipDirective,
skipDirectiveNode,
variableValues,
fragmentVariableValues,
hideSuggestions,
)
) {
continue;
}

const fragment = fragments[fragName];
if (
fragment == null ||
!doesFragmentConditionMatch(schema, fragment.definition, runtimeType)
) {
continue;
}
: undefined;
if (skip?.if === true) {
return false;
}

const newDeferUsage = getDeferUsage(
variableValues,
fragmentVariableValues,
selection,
deferUsage,
);

const fragmentVariableSignatures = fragment.variableSignatures;
let newFragmentVariableValues: FragmentVariableValues | undefined;
if (fragmentVariableSignatures) {
newFragmentVariableValues = getFragmentVariableValues(
selection,
fragmentVariableSignatures,
const includeDirectiveNode = node.directives?.find(
(directive) => directive.name.value === GraphQLIncludeDirective.name,
);
if (includeDirectiveNode && forbidSkipAndInclude) {
forbiddenDirectiveInstances.push(includeDirectiveNode);
return false;
}
const include = includeDirectiveNode
? getArgumentValues(
GraphQLIncludeDirective,
includeDirectiveNode,
variableValues,
fragmentVariableValues,
hideSuggestions,
);
}

if (!newDeferUsage) {
visitedFragmentNames.add(fragName);
collectFieldsImpl(
context,
fragment.definition.selectionSet,
groupedFieldSet,
newDeferUsages,
deferUsage,
newFragmentVariableValues,
);
} else {
newDeferUsages.push(newDeferUsage);
collectFieldsImpl(
context,
fragment.definition.selectionSet,
groupedFieldSet,
newDeferUsages,
newDeferUsage,
newFragmentVariableValues,
);
}
break;
)
: undefined;
if (include?.if === false) {
return false;
}
return true;
}
}
}

/**
* Returns an object containing the `@defer` arguments if a field should be
* deferred based on the experimental flag, defer directive present and
* not disabled by the "if" argument.
*/
function getDeferUsage(
variableValues: VariableValues,
fragmentVariableValues: FragmentVariableValues | undefined,
node: FragmentSpreadNode | InlineFragmentNode,
parentDeferUsage: DeferUsage | undefined,
): DeferUsage | undefined {
const defer = getDirectiveValues(
GraphQLDeferDirective,
node,
variableValues,
fragmentVariableValues,
);

if (!defer) {
return;
}

if (defer.if === false) {
return;
}

return {
label: typeof defer.label === 'string' ? defer.label : undefined,
parentDeferUsage,
};
}

/**
* Determines if a field should be included based on the `@include` and `@skip`
* directives, where `@skip` has higher precedence than `@include`.
*/
function shouldIncludeNode(
context: CollectFieldsContext,
node: FragmentSpreadNode | FieldNode | InlineFragmentNode,
variableValues: VariableValues,
fragmentVariableValues: FragmentVariableValues | undefined,
): boolean {
const skipDirectiveNode = node.directives?.find(
(directive) => directive.name.value === GraphQLSkipDirective.name,
);
if (skipDirectiveNode && context.forbidSkipAndInclude) {
context.forbiddenDirectiveInstances.push(skipDirectiveNode);
return false;
}
const skip = skipDirectiveNode
? getArgumentValues(
GraphQLSkipDirective,
skipDirectiveNode,
variableValues,
fragmentVariableValues,
context.hideSuggestions,
)
: undefined;
if (skip?.if === true) {
return false;
}

const includeDirectiveNode = node.directives?.find(
(directive) => directive.name.value === GraphQLIncludeDirective.name,
);
if (includeDirectiveNode && context.forbidSkipAndInclude) {
context.forbiddenDirectiveInstances.push(includeDirectiveNode);
return false;
}
const include = includeDirectiveNode
? getArgumentValues(
GraphQLIncludeDirective,
includeDirectiveNode,
variableValues,
fragmentVariableValues,
context.hideSuggestions,
)
: undefined;
if (include?.if === false) {
/**
* Determines if a fragment is applicable to the given type.
*/
function doesFragmentConditionMatch(
fragment: FragmentDefinitionNode | InlineFragmentNode,
): boolean {
const typeConditionNode = fragment.typeCondition;
if (!typeConditionNode) {
return true;
}
const conditionalType = typeFromAST(schema, typeConditionNode);
if (conditionalType === runtimeType) {
return true;
}
if (isAbstractType(conditionalType)) {
return schema.isSubType(conditionalType, runtimeType);
}
return false;
}
return true;
}

/**
* Determines if a fragment is applicable to the given type.
*/
function doesFragmentConditionMatch(
schema: GraphQLSchema,
fragment: FragmentDefinitionNode | InlineFragmentNode,
type: GraphQLObjectType,
): boolean {
const typeConditionNode = fragment.typeCondition;
if (!typeConditionNode) {
return true;
}
const conditionalType = typeFromAST(schema, typeConditionNode);
if (conditionalType === type) {
return true;
}
if (isAbstractType(conditionalType)) {
return schema.isSubType(conditionalType, type);
}
return false;
return selectionSetVisitor;
}

/**