Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(graphqlsp): Fix infinite loop conditions when resolving fragments (#350)

authored by

Phil Pluckthun and committed by
GitHub
1ef3b51b ad02ab14

+73 -53
+5
.changeset/fuzzy-scissors-appear.md
··· 1 + --- 2 + '@0no-co/graphqlsp': patch 3 + --- 4 + 5 + Prevent resolution loop when resolving GraphQL fragments
+68 -53
packages/graphqlsp/src/ast/index.ts
··· 49 49 return result; 50 50 } 51 51 52 - function unrollFragment( 53 - element: ts.Identifier, 52 + function resolveIdentifierToGraphQLCall( 53 + input: ts.Identifier, 54 54 info: ts.server.PluginCreateInfo, 55 - typeChecker: ts.TypeChecker | undefined 56 - ): Array<FragmentDefinitionNode> { 57 - const fragments: Array<FragmentDefinitionNode> = []; 58 - const definitions = info.languageService.getDefinitionAtPosition( 59 - element.getSourceFile().fileName, 60 - element.getStart() 61 - ); 55 + checker: ts.TypeChecker | undefined 56 + ): checks.GraphQLCallNode | null { 57 + let prevElement: ts.Node | undefined; 58 + let element: ts.Node | undefined = input; 59 + // NOTE: Under certain circumstances, resolving an identifier can loop 60 + while (ts.isIdentifier(element) && element !== prevElement) { 61 + prevElement = element; 62 62 63 - const fragment = definitions && definitions[0]; 64 - if (!fragment) return fragments; 63 + const definitions = info.languageService.getDefinitionAtPosition( 64 + element.getSourceFile().fileName, 65 + element.getStart() 66 + ); 65 67 66 - const externalSource = getSource(info, fragment.fileName); 67 - if (!externalSource) return fragments; 68 + const fragment = definitions && definitions[0]; 69 + const externalSource = fragment && getSource(info, fragment.fileName); 70 + if (!fragment || !externalSource) return null; 68 71 69 - let found = findNode(externalSource, fragment.textSpan.start); 70 - if (!found) return fragments; 72 + element = findNode(externalSource, fragment.textSpan.start); 73 + if (!element) return null; 71 74 72 - while (ts.isPropertyAccessExpression(found.parent)) found = found.parent; 75 + while (ts.isPropertyAccessExpression(element.parent)) 76 + element = element.parent; 73 77 74 - if ( 75 - ts.isVariableDeclaration(found.parent) && 76 - found.parent.initializer && 77 - ts.isCallExpression(found.parent.initializer) 78 - ) { 79 - found = found.parent.initializer; 80 - } else if (ts.isPropertyAssignment(found.parent)) { 81 - found = found.parent.initializer; 82 - } else if (ts.isBinaryExpression(found.parent)) { 83 - if (ts.isPropertyAccessExpression(found.parent.right)) { 84 - found = found.parent.right.name as ts.Identifier; 85 - } else { 86 - found = found.parent.right; 78 + if ( 79 + ts.isVariableDeclaration(element.parent) && 80 + element.parent.initializer && 81 + ts.isCallExpression(element.parent.initializer) 82 + ) { 83 + element = element.parent.initializer; 84 + } else if (ts.isPropertyAssignment(element.parent)) { 85 + element = element.parent.initializer; 86 + } else if (ts.isBinaryExpression(element.parent)) { 87 + element = ts.isPropertyAccessExpression(element.parent.right) 88 + ? element.parent.right.name 89 + : element.parent.right; 87 90 } 88 - } 89 - 90 - // If we found another identifier, we repeat trying to find the original 91 - // fragment definition 92 - if (ts.isIdentifier(found)) { 93 - return unrollFragment(found, info, typeChecker); 91 + // If we find another Identifier, we continue resolving it 94 92 } 95 - 96 93 // Check whether we've got a `graphql()` or `gql()` call, by the 97 94 // call expression's identifier 98 - if (!checks.isGraphQLCall(found, typeChecker)) { 99 - return fragments; 100 - } 95 + return checks.isGraphQLCall(element, checker) ? element : null; 96 + } 101 97 102 - try { 103 - const text = found.arguments[0]; 104 - const fragmentRefs = resolveTadaFragmentArray(found.arguments[1]); 105 - if (fragmentRefs) { 106 - for (const identifier of fragmentRefs) { 107 - fragments.push(...unrollFragment(identifier, info, typeChecker)); 108 - } 98 + function unrollFragment( 99 + element: ts.Identifier, 100 + info: ts.server.PluginCreateInfo, 101 + checker: ts.TypeChecker | undefined 102 + ): Array<FragmentDefinitionNode> { 103 + const fragments: FragmentDefinitionNode[] = []; 104 + const elements: ts.Identifier[] = [element]; 105 + const seen = new WeakSet<ts.Identifier>(); 106 + 107 + const _unrollElement = (element: ts.Identifier): void => { 108 + if (seen.has(element)) return; 109 + seen.add(element); 110 + 111 + const node = resolveIdentifierToGraphQLCall(element, info, checker); 112 + if (!node) return; 113 + 114 + const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]); 115 + if (fragmentRefs) elements.push(...fragmentRefs); 116 + 117 + try { 118 + const text = node.arguments[0]; 119 + const parsed = parse(text.getText().slice(1, -1), { noLocation: true }); 120 + parsed.definitions.forEach(definition => { 121 + if (definition.kind === 'FragmentDefinition') { 122 + fragments.push(definition); 123 + } 124 + }); 125 + } catch (_error) { 126 + // NOTE: Assume graphql.parse errors can be ignored 109 127 } 110 - const parsed = parse(text.getText().slice(1, -1), { noLocation: true }); 111 - parsed.definitions.forEach(definition => { 112 - if (definition.kind === 'FragmentDefinition') { 113 - fragments.push(definition); 114 - } 115 - }); 116 - } catch (e) {} 128 + }; 117 129 130 + let nextElement: ts.Identifier | undefined; 131 + while ((nextElement = elements.shift()) !== undefined) 132 + _unrollElement(nextElement); 118 133 return fragments; 119 134 } 120 135