diff --git a/.changeset/fuzzy-scissors-appear.md b/.changeset/fuzzy-scissors-appear.md new file mode 100644 index 00000000..e2c0f580 --- /dev/null +++ b/.changeset/fuzzy-scissors-appear.md @@ -0,0 +1,5 @@ +--- +'@0no-co/graphqlsp': patch +--- + +Prevent resolution loop when resolving GraphQL fragments diff --git a/packages/graphqlsp/src/ast/index.ts b/packages/graphqlsp/src/ast/index.ts index a3e17e28..5d3458dc 100644 --- a/packages/graphqlsp/src/ast/index.ts +++ b/packages/graphqlsp/src/ast/index.ts @@ -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 { - const fragments: Array = []; - 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 { + const fragments: FragmentDefinitionNode[] = []; + const elements: ts.Identifier[] = [element]; + const seen = new WeakSet(); + + 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; }