Skip to content

Commit

Permalink
fix(graphqlsp): Fix infinite loop conditions when resolving fragments (
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Aug 22, 2024
1 parent ad02ab1 commit 1ef3b51
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/fuzzy-scissors-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphqlsp': patch
---

Prevent resolution loop when resolving GraphQL fragments
121 changes: 68 additions & 53 deletions packages/graphqlsp/src/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,72 +49,87 @@ export function findAllTaggedTemplateNodes(
return result;
}

function unrollFragment(
element: ts.Identifier,
function resolveIdentifierToGraphQLCall(
input: ts.Identifier,
info: ts.server.PluginCreateInfo,
typeChecker: ts.TypeChecker | undefined
): Array<FragmentDefinitionNode> {
const fragments: Array<FragmentDefinitionNode> = [];
const definitions = info.languageService.getDefinitionAtPosition(
element.getSourceFile().fileName,
element.getStart()
);
checker: ts.TypeChecker | undefined
): checks.GraphQLCallNode | null {
let prevElement: ts.Node | undefined;
let element: ts.Node | undefined = input;
// NOTE: Under certain circumstances, resolving an identifier can loop
while (ts.isIdentifier(element) && element !== prevElement) {
prevElement = element;

const fragment = definitions && definitions[0];
if (!fragment) return fragments;
const definitions = info.languageService.getDefinitionAtPosition(
element.getSourceFile().fileName,
element.getStart()
);

const externalSource = getSource(info, fragment.fileName);
if (!externalSource) return fragments;
const fragment = definitions && definitions[0];
const externalSource = fragment && getSource(info, fragment.fileName);
if (!fragment || !externalSource) return null;

let found = findNode(externalSource, fragment.textSpan.start);
if (!found) return fragments;
element = findNode(externalSource, fragment.textSpan.start);
if (!element) return null;

while (ts.isPropertyAccessExpression(found.parent)) found = found.parent;
while (ts.isPropertyAccessExpression(element.parent))
element = element.parent;

if (
ts.isVariableDeclaration(found.parent) &&
found.parent.initializer &&
ts.isCallExpression(found.parent.initializer)
) {
found = found.parent.initializer;
} else if (ts.isPropertyAssignment(found.parent)) {
found = found.parent.initializer;
} else if (ts.isBinaryExpression(found.parent)) {
if (ts.isPropertyAccessExpression(found.parent.right)) {
found = found.parent.right.name as ts.Identifier;
} else {
found = found.parent.right;
if (
ts.isVariableDeclaration(element.parent) &&
element.parent.initializer &&
ts.isCallExpression(element.parent.initializer)
) {
element = element.parent.initializer;
} else if (ts.isPropertyAssignment(element.parent)) {
element = element.parent.initializer;
} else if (ts.isBinaryExpression(element.parent)) {
element = ts.isPropertyAccessExpression(element.parent.right)
? element.parent.right.name
: element.parent.right;
}
// If we find another Identifier, we continue resolving it
}

// If we found another identifier, we repeat trying to find the original
// fragment definition
if (ts.isIdentifier(found)) {
return unrollFragment(found, info, typeChecker);
}

// Check whether we've got a `graphql()` or `gql()` call, by the
// call expression's identifier
if (!checks.isGraphQLCall(found, typeChecker)) {
return fragments;
}
return checks.isGraphQLCall(element, checker) ? element : null;
}

try {
const text = found.arguments[0];
const fragmentRefs = resolveTadaFragmentArray(found.arguments[1]);
if (fragmentRefs) {
for (const identifier of fragmentRefs) {
fragments.push(...unrollFragment(identifier, info, typeChecker));
}
function unrollFragment(
element: ts.Identifier,
info: ts.server.PluginCreateInfo,
checker: ts.TypeChecker | undefined
): Array<FragmentDefinitionNode> {
const fragments: FragmentDefinitionNode[] = [];
const elements: ts.Identifier[] = [element];
const seen = new WeakSet<ts.Identifier>();

const _unrollElement = (element: ts.Identifier): void => {
if (seen.has(element)) return;
seen.add(element);

const node = resolveIdentifierToGraphQLCall(element, info, checker);
if (!node) return;

const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]);
if (fragmentRefs) elements.push(...fragmentRefs);

try {
const text = node.arguments[0];
const parsed = parse(text.getText().slice(1, -1), { noLocation: true });
parsed.definitions.forEach(definition => {
if (definition.kind === 'FragmentDefinition') {
fragments.push(definition);
}
});
} catch (_error) {
// NOTE: Assume graphql.parse errors can be ignored
}
const parsed = parse(text.getText().slice(1, -1), { noLocation: true });
parsed.definitions.forEach(definition => {
if (definition.kind === 'FragmentDefinition') {
fragments.push(definition);
}
});
} catch (e) {}
};

let nextElement: ts.Identifier | undefined;
while ((nextElement = elements.shift()) !== undefined)
_unrollElement(nextElement);
return fragments;
}

Expand Down

0 comments on commit 1ef3b51

Please sign in to comment.