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

fix(16.x.x.): avoid stack size limit in overlapping field rules as well as execute #4116

Closed
41 changes: 41 additions & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,47 @@ describe('Execute: Handles basic execution tasks', () => {
);
});

it('works with deeply nested fragments', async () => {
const DataType: GraphQLObjectType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
a: { type: GraphQLString, resolve: () => 'Apple' },
}),
});

const n = 10000;
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const document = parse(`
query {
...X${n}
}
${fragments}
fragment X0 on Query {
a
}
`);

const result = await execute({
schema: new GraphQLSchema({ query: DataType }),
document,
});

expect(result).to.deep.equal({
data: {
a: 'Apple',
},
});
});

it('executes arbitrary code', async () => {
const data = {
a: () => 'Apple',
Expand Down
55 changes: 45 additions & 10 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { typeFromAST } from '../utilities/typeFromAST';

import { getDirectiveValues } from './values';

interface FragmentEntry {
fragment: FragmentDefinitionNode;
runtimeType: GraphQLObjectType;
}

/**
* Given a selectionSet, collects all of the fields and returns them.
*
Expand All @@ -37,16 +42,34 @@ export function collectFields(
runtimeType: GraphQLObjectType,
selectionSet: SelectionSetNode,
): Map<string, ReadonlyArray<FieldNode>> {
const foundFragments: Array<FragmentEntry> = [];
const fields = new Map();
const visited = new Set<string>();
collectFieldsImpl(
schema,
fragments,
variableValues,
runtimeType,
selectionSet,
fields,
new Set(),
visited,
foundFragments,
);

let entry;
while ((entry = foundFragments.pop()) !== undefined) {
yaacovCR marked this conversation as resolved.
Show resolved Hide resolved
collectFieldsImpl(
schema,
fragments,
variableValues,
entry.runtimeType,
entry.fragment.selectionSet,
fields,
visited,
foundFragments,
);
}

return fields;
}

Expand All @@ -68,6 +91,7 @@ export function collectSubfields(
fieldNodes: ReadonlyArray<FieldNode>,
): Map<string, ReadonlyArray<FieldNode>> {
const subFieldNodes = new Map();
const foundFragments: Array<FragmentEntry> = [];
const visitedFragmentNames = new Set<string>();
for (const node of fieldNodes) {
if (node.selectionSet) {
Expand All @@ -79,9 +103,25 @@ export function collectSubfields(
node.selectionSet,
subFieldNodes,
visitedFragmentNames,
foundFragments,
);
}
}

let entry;
while ((entry = foundFragments.pop()) !== undefined) {
collectFieldsImpl(
schema,
fragments,
variableValues,
entry.runtimeType,
entry.fragment.selectionSet,
subFieldNodes,
visitedFragmentNames,
foundFragments,
);
}

return subFieldNodes;
}

Expand All @@ -93,6 +133,7 @@ function collectFieldsImpl(
selectionSet: SelectionSetNode,
fields: Map<string, Array<FieldNode>>,
visitedFragmentNames: Set<string>,
foundFragments: Array<FragmentEntry>,
): void {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
Expand Down Expand Up @@ -124,6 +165,7 @@ function collectFieldsImpl(
selection.selectionSet,
fields,
visitedFragmentNames,
foundFragments,
);
break;
}
Expand All @@ -143,15 +185,8 @@ function collectFieldsImpl(
) {
continue;
}
collectFieldsImpl(
schema,
fragments,
variableValues,
runtimeType,
fragment.selectionSet,
fields,
visitedFragmentNames,
);

foundFragments.push({ runtimeType, fragment });
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1138,4 +1138,70 @@ describe('Validate: Overlapping fields can be merged', () => {
},
]);
});

it('does not hit stack size limits', () => {
const n = 10000;
JoviDeCroock marked this conversation as resolved.
Show resolved Hide resolved
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const query = `
query Test {
...X${n}
}
${fragments}
fragment X0 on Query {
__typename
}
`;

expectErrors(query).toDeepEqual([]);
});

it('finds conflicts in nested fragments', () => {
const n = 10000;
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const query = `
query Test {
type: conflict
...X${n}
}
${fragments}
fragment X0 on Query {
type: conflict2
__typename
}
`;
expectErrors(query).toDeepEqual([
{
locations: [
{
column: 9,
line: 3,
},
{
column: 9,
line: 50008,
},
],
message:
'Fields "type" conflict because "conflict" and "conflict2" are different fields. Use different aliases on the fields to fetch both if this was intentional.',
},
]);
});
});
Loading
Loading