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.

patch: Increase support of harder to detect `gql.tada` API usage patterns (#309)

authored by

Phil Pluckthun and committed by
GitHub
eb7ce755 e314fd2d

+255 -172
+5
.changeset/young-phones-search.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Expand support for `gql.tada` API. GraphQLSP will now recognize `graphql()`/`graphql.persisted()` calls regardless of variable naming and support more obscure usage patterns.
+116
packages/graphqlsp/src/ast/checks.ts
··· 1 + import { ts } from '../ts'; 2 + import { templates } from './templates'; 3 + 4 + /** Checks for an immediately-invoked function expression */ 5 + export const isIIFE = (node: ts.Node): boolean => 6 + ts.isCallExpression(node) && 7 + node.arguments.length === 0 && 8 + (ts.isFunctionExpression(node.expression) || 9 + ts.isArrowFunction(node.expression)) && 10 + !node.expression.asteriskToken && 11 + !node.expression.modifiers?.length; 12 + 13 + /** Checks if node is a known identifier of graphql functions ('graphql' or 'gql') */ 14 + export const isGraphQLFunctionIdentifier = ( 15 + node: ts.Node 16 + ): node is ts.Identifier => 17 + ts.isIdentifier(node) && templates.has(node.escapedText as string); 18 + 19 + /** If `checker` is passed, checks if node (as identifier/expression) is a gql.tada graphql() function */ 20 + export const isTadaGraphQLFunction = ( 21 + node: ts.Node, 22 + checker: ts.TypeChecker | undefined 23 + ): node is ts.LeftHandSideExpression => { 24 + if (!ts.isLeftHandSideExpression(node)) return false; 25 + const type = checker?.getTypeAtLocation(node); 26 + // Any function that has both a `scalar` and `persisted` property 27 + // is automatically considered a gql.tada graphql() function. 28 + return ( 29 + type != null && 30 + type.getProperty('scalar') != null && 31 + type.getProperty('persisted') != null 32 + ); 33 + }; 34 + 35 + /** If `checker` is passed, checks if node is a gql.tada graphql() call */ 36 + export const isTadaGraphQLCall = ( 37 + node: ts.CallExpression, 38 + checker: ts.TypeChecker | undefined 39 + ): boolean => { 40 + // We expect graphql() to be called with either a string literal 41 + // or a string literal and an array of fragments 42 + if (!ts.isCallExpression(node)) { 43 + return false; 44 + } else if (node.arguments.length < 1 || node.arguments.length > 2) { 45 + return false; 46 + } else if (!ts.isStringLiteralLike(node.arguments[0])) { 47 + return false; 48 + } 49 + return checker ? isTadaGraphQLFunction(node.expression, checker) : false; 50 + }; 51 + 52 + /** Checks if node is a gql.tada graphql.persisted() call */ 53 + export const isTadaPersistedCall = ( 54 + node: ts.Node, 55 + checker: ts.TypeChecker | undefined 56 + ): node is ts.CallExpression => { 57 + if (!ts.isCallExpression(node)) { 58 + return false; 59 + } else if (!ts.isPropertyAccessExpression(node.expression)) { 60 + return false; // rejecting non property access calls: <expression>.<name>() 61 + } else if ( 62 + !ts.isIdentifier(node.expression.name) || 63 + node.expression.name.escapedText !== 'persisted' 64 + ) { 65 + return false; // rejecting calls on anyting but 'persisted': <expression>.persisted() 66 + } else if (isGraphQLFunctionIdentifier(node.expression.expression)) { 67 + return true; 68 + } else { 69 + return isTadaGraphQLFunction(node.expression.expression, checker); 70 + } 71 + }; 72 + 73 + /** Checks if node is a gql.tada or regular graphql() call */ 74 + export const isGraphQLCall = ( 75 + node: ts.Node, 76 + checker: ts.TypeChecker | undefined 77 + ): node is ts.CallExpression => { 78 + return ( 79 + ts.isCallExpression(node) && 80 + node.arguments.length >= 1 && 81 + node.arguments.length <= 2 && 82 + (isGraphQLFunctionIdentifier(node.expression) || 83 + isTadaGraphQLCall(node, checker)) 84 + ); 85 + }; 86 + 87 + /** Checks if node is a gql/graphql tagged template literal */ 88 + export const isGraphQLTag = ( 89 + node: ts.Node 90 + ): node is ts.TaggedTemplateExpression => 91 + ts.isTaggedTemplateExpression(node) && isGraphQLFunctionIdentifier(node.tag); 92 + 93 + /** Retrieves the `__name` branded tag from gql.tada `graphql()` or `graphql.persisted()` calls */ 94 + export const getSchemaName = ( 95 + node: ts.CallExpression, 96 + typeChecker: ts.TypeChecker | undefined 97 + ): string | null => { 98 + if (!typeChecker) return null; 99 + const expression = ts.isPropertyAccessExpression(node.expression) 100 + ? node.expression.expression 101 + : node.expression; 102 + const type = typeChecker.getTypeAtLocation(expression); 103 + if (type) { 104 + const brandTypeSymbol = type.getProperty('__name'); 105 + if (brandTypeSymbol) { 106 + const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol); 107 + if (brand.isUnionOrIntersection()) { 108 + const found = brand.types.find(x => x.isStringLiteral()); 109 + return found && found.isStringLiteral() ? found.value : null; 110 + } else if (brand.isStringLiteral()) { 111 + return brand.value; 112 + } 113 + } 114 + } 115 + return null; 116 + };
+84 -113
packages/graphqlsp/src/ast/index.ts
··· 1 1 import { ts } from '../ts'; 2 2 import { FragmentDefinitionNode, parse } from 'graphql'; 3 - import { templates } from './templates'; 3 + import * as checks from './checks'; 4 + import { resolveTadaFragmentArray } from './resolve'; 5 + 6 + export { getSchemaName } from './checks'; 4 7 5 8 export function getSource(info: ts.server.PluginCreateInfo, filename: string) { 6 9 const program = info.languageService.getProgram(); ··· 32 35 > = []; 33 36 function find(node: ts.Node) { 34 37 if ( 35 - (ts.isTaggedTemplateExpression(node) && 36 - templates.has(node.tag.getText())) || 38 + checks.isGraphQLTag(node) || 37 39 (ts.isNoSubstitutionTemplateLiteral(node) && 38 - ts.isTaggedTemplateExpression(node.parent) && 39 - templates.has(node.parent.tag.getText())) 40 + checks.isGraphQLTag(node.parent)) 40 41 ) { 41 42 result.push(node); 42 43 return; ··· 50 51 51 52 function unrollFragment( 52 53 element: ts.Identifier, 53 - info: ts.server.PluginCreateInfo 54 + info: ts.server.PluginCreateInfo, 55 + typeChecker: ts.TypeChecker | undefined 54 56 ): Array<FragmentDefinitionNode> { 55 57 const fragments: Array<FragmentDefinitionNode> = []; 56 58 const definitions = info.languageService.getDefinitionAtPosition( ··· 78 80 found = found.parent.initializer; 79 81 } 80 82 81 - if (ts.isCallExpression(found) && templates.has(found.expression.getText())) { 82 - const [arg, arg2] = found.arguments; 83 - if (arg2 && ts.isArrayLiteralExpression(arg2)) { 84 - arg2.elements.forEach(element => { 85 - if (ts.isIdentifier(element)) { 86 - fragments.push(...unrollFragment(element, info)); 87 - } 88 - }); 83 + // Check whether we've got a `graphql()` or `gql()` call, by the 84 + // call expression's identifier 85 + if (!checks.isGraphQLCall(found, typeChecker)) { 86 + return fragments; 87 + } 88 + 89 + try { 90 + const text = found.arguments[0]; 91 + const fragmentRefs = resolveTadaFragmentArray(found.arguments[1]); 92 + if (fragmentRefs) { 93 + for (const identifier of fragmentRefs) { 94 + fragments.push(...unrollFragment(identifier, info, typeChecker)); 95 + } 89 96 } 90 - 91 - try { 92 - const parsed = parse(arg.getText().slice(1, -1), { noLocation: true }); 93 - parsed.definitions.forEach(definition => { 94 - if (definition.kind === 'FragmentDefinition') { 95 - fragments.push(definition); 96 - } 97 - }); 98 - } catch (e) {} 99 - } 97 + const parsed = parse(text.getText().slice(1, -1), { noLocation: true }); 98 + parsed.definitions.forEach(definition => { 99 + if (definition.kind === 'FragmentDefinition') { 100 + fragments.push(definition); 101 + } 102 + }); 103 + } catch (e) {} 100 104 101 105 return fragments; 102 106 } ··· 106 110 wip: FragmentDefinitionNode[], 107 111 info: ts.server.PluginCreateInfo 108 112 ): FragmentDefinitionNode[] { 113 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 109 114 fragmentsArray.elements.forEach(element => { 110 115 if (ts.isIdentifier(element)) { 111 - wip.push(...unrollFragment(element, info)); 116 + wip.push(...unrollFragment(element, info, typeChecker)); 112 117 } else if (ts.isPropertyAccessExpression(element)) { 113 118 let el = element; 114 - while (ts.isPropertyAccessExpression(el.expression)) { 115 - el = el.expression; 116 - } 117 - 119 + while (ts.isPropertyAccessExpression(el.expression)) el = el.expression; 118 120 if (ts.isIdentifier(el.name)) { 119 - wip.push(...unrollFragment(el.name, info)); 121 + wip.push(...unrollFragment(el.name, info, typeChecker)); 120 122 } 121 123 } 122 124 }); ··· 124 126 return wip; 125 127 } 126 128 127 - export const getSchemaName = ( 128 - node: ts.CallExpression, 129 - typeChecker?: ts.TypeChecker 130 - ): string | null => { 131 - if (!typeChecker) return null; 132 - 133 - const expression = ts.isPropertyAccessExpression(node.expression) 134 - ? node.expression.expression 135 - : node.expression; 136 - const type = typeChecker.getTypeAtLocation(expression); 137 - if (type) { 138 - const brandTypeSymbol = type.getProperty('__name'); 139 - if (brandTypeSymbol) { 140 - const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol); 141 - if (brand.isUnionOrIntersection()) { 142 - const found = brand.types.find(x => x.isStringLiteral()); 143 - return found && found.isStringLiteral() ? found.value : null; 144 - } else if (brand.isStringLiteral()) { 145 - return brand.value; 146 - } 147 - } 148 - } 149 - 150 - return null; 151 - }; 152 - 153 129 export function findAllCallExpressions( 154 130 sourceFile: ts.SourceFile, 155 131 info: ts.server.PluginCreateInfo, 156 132 shouldSearchFragments: boolean = true 157 133 ): { 158 134 nodes: Array<{ 159 - node: ts.NoSubstitutionTemplateLiteral; 135 + node: ts.StringLiteralLike; 160 136 schema: string | null; 161 137 }>; 162 138 fragments: Array<FragmentDefinitionNode>; 163 139 } { 164 140 const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 165 141 const result: Array<{ 166 - node: ts.NoSubstitutionTemplateLiteral; 142 + node: ts.StringLiteralLike; 167 143 schema: string | null; 168 144 }> = []; 169 145 let fragments: Array<FragmentDefinitionNode> = []; 170 146 let hasTriedToFindFragments = shouldSearchFragments ? false : true; 171 - function find(node: ts.Node) { 172 - if (ts.isCallExpression(node) && templates.has(node.expression.getText())) { 173 - const name = getSchemaName(node, typeChecker); 147 + 148 + function find(node: ts.Node): void { 149 + if (!ts.isCallExpression(node) || checks.isIIFE(node)) { 150 + return ts.forEachChild(node, find); 151 + } 174 152 175 - const [arg, arg2] = node.arguments; 153 + // Check whether we've got a `graphql()` or `gql()` call, by the 154 + // call expression's identifier 155 + if (!checks.isGraphQLCall(node, typeChecker)) { 156 + return; 157 + } 176 158 177 - if (!hasTriedToFindFragments && !arg2) { 178 - hasTriedToFindFragments = true; 179 - fragments.push(...getAllFragments(sourceFile.fileName, node, info)); 180 - } else if (arg2 && ts.isArrayLiteralExpression(arg2)) { 181 - arg2.elements.forEach(element => { 182 - if (ts.isIdentifier(element)) { 183 - fragments.push(...unrollFragment(element, info)); 184 - } else if (ts.isPropertyAccessExpression(element)) { 185 - let el = element; 186 - while (ts.isPropertyAccessExpression(el.expression)) { 187 - el = el.expression; 188 - } 159 + const name = checks.getSchemaName(node, typeChecker); 160 + const text = node.arguments[0]; 161 + const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]); 189 162 190 - if (ts.isIdentifier(el.name)) { 191 - fragments.push(...unrollFragment(el.name, info)); 192 - } 193 - } 194 - }); 163 + if (!hasTriedToFindFragments && !fragmentRefs) { 164 + hasTriedToFindFragments = true; 165 + fragments.push(...getAllFragments(sourceFile.fileName, node, info)); 166 + } else if (fragmentRefs) { 167 + for (const identifier of fragmentRefs) { 168 + fragments.push(...unrollFragment(identifier, info, typeChecker)); 195 169 } 170 + } 196 171 197 - if (arg && ts.isNoSubstitutionTemplateLiteral(arg)) { 198 - result.push({ node: arg, schema: name }); 199 - } 200 - return; 201 - } else { 202 - ts.forEachChild(node, find); 172 + if (text && ts.isStringLiteralLike(text)) { 173 + result.push({ node: text, schema: name }); 203 174 } 204 175 } 205 176 find(sourceFile); ··· 222 193 ts.CallExpression | { node: ts.CallExpression; schema: string | null } 223 194 > = []; 224 195 const typeChecker = info?.languageService.getProgram()?.getTypeChecker(); 225 - function find(node: ts.Node) { 226 - if (node && ts.isCallExpression(node)) { 227 - // This expression ideally for us looks like <template>.persisted 228 - const expression = node.expression.getText(); 229 - const parts = expression.split('.'); 230 - if (parts.length !== 2) return; 231 - 232 - const [template, method] = parts; 233 - if (!templates.has(template) || method !== 'persisted') return; 196 + function find(node: ts.Node): void { 197 + if (!ts.isCallExpression(node) || checks.isIIFE(node)) { 198 + return ts.forEachChild(node, find); 199 + } 234 200 235 - if (info) { 236 - const name = getSchemaName(node, typeChecker); 237 - result.push({ node, schema: name }); 238 - } else { 239 - result.push(node); 240 - } 201 + if (!checks.isTadaPersistedCall(node, typeChecker)) { 202 + return; 203 + } else if (info) { 204 + const name = checks.getSchemaName(node, typeChecker); 205 + result.push({ node, schema: name }); 241 206 } else { 242 - ts.forEachChild(node, find); 207 + result.push(node); 243 208 } 244 209 } 245 210 find(sourceFile); ··· 248 213 249 214 export function getAllFragments( 250 215 fileName: string, 251 - node: ts.CallExpression, 216 + node: ts.Node, 252 217 info: ts.server.PluginCreateInfo 253 218 ) { 254 219 let fragments: Array<FragmentDefinitionNode> = []; 255 220 221 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 222 + if (!ts.isCallExpression(node)) { 223 + return fragments; 224 + } 225 + 226 + const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]); 227 + if (fragmentRefs) { 228 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 229 + for (const identifier of fragmentRefs) { 230 + fragments.push(...unrollFragment(identifier, info, typeChecker)); 231 + } 232 + return fragments; 233 + } else if (checks.isTadaGraphQLCall(node, typeChecker)) { 234 + return fragments; 235 + } 236 + 256 237 const definitions = info.languageService.getDefinitionAtPosition( 257 238 fileName, 258 239 node.expression.getStart() 259 240 ); 260 241 if (!definitions || !definitions.length) return fragments; 261 - 262 - if (node.arguments[1] && ts.isArrayLiteralExpression(node.arguments[1])) { 263 - const arg2 = node.arguments[1] as ts.ArrayLiteralExpression; 264 - arg2.elements.forEach(element => { 265 - if (ts.isIdentifier(element)) { 266 - fragments.push(...unrollFragment(element, info)); 267 - } 268 - }); 269 - return fragments; 270 - } 271 242 272 243 const def = definitions[0]; 273 244 if (!def) return fragments; ··· 339 310 340 311 export function bubbleUpCallExpression(node: ts.Node): ts.Node { 341 312 while ( 342 - ts.isNoSubstitutionTemplateLiteral(node) || 313 + ts.isStringLiteralLike(node) || 343 314 ts.isToken(node) || 344 315 ts.isTemplateExpression(node) || 345 316 ts.isTemplateSpan(node)
+19 -2
packages/graphqlsp/src/ast/resolve.ts
··· 14 14 }; 15 15 16 16 export function resolveTemplate( 17 - node: ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral, 17 + node: ts.TaggedTemplateExpression | ts.StringLiteralLike, 18 18 filename: string, 19 19 info: ts.server.PluginCreateInfo 20 20 ): TemplateResult { 21 - if (ts.isNoSubstitutionTemplateLiteral(node)) { 21 + if (ts.isStringLiteralLike(node)) { 22 22 return { combinedText: node.getText().slice(1, -1), resolvedSpans: [] }; 23 23 } 24 24 ··· 146 146 147 147 return { combinedText: templateText, resolvedSpans }; 148 148 } 149 + 150 + export const resolveTadaFragmentArray = ( 151 + node: ts.Expression | undefined 152 + ): undefined | readonly ts.Identifier[] => { 153 + if (!node) return undefined; 154 + // NOTE: Remove `as T`, users may commonly use `as const` for no reason 155 + while (ts.isAsExpression(node)) node = node.expression; 156 + if (!ts.isArrayLiteralExpression(node)) return undefined; 157 + // NOTE: Let's avoid the allocation of another array here if we can 158 + if (node.elements.every(ts.isIdentifier)) return node.elements; 159 + const identifiers: ts.Identifier[] = []; 160 + for (let element of node.elements) { 161 + while (ts.isPropertyAccessExpression(element)) element = element.expression; 162 + if (ts.isIdentifier(element)) identifiers.push(element); 163 + } 164 + return identifiers; 165 + };
+5 -1
packages/graphqlsp/src/ast/token.ts
··· 11 11 } 12 12 13 13 export const getToken = ( 14 - template: ts.TemplateLiteral, 14 + template: ts.Expression, 15 15 cursorPosition: number 16 16 ): Token | undefined => { 17 + if (!ts.isTemplateLiteral(template) && !ts.isStringLiteralLike(template)) { 18 + return undefined; 19 + } 20 + 17 21 const text = template.getText().slice(1, -1); 18 22 const input = text.split('\n'); 19 23 const parser = onlineParser();
+6 -18
packages/graphqlsp/src/autoComplete.ts
··· 15 15 import { FragmentDefinitionNode, GraphQLSchema, Kind, parse } from 'graphql'; 16 16 import { print } from '@0no-co/graphql.web'; 17 17 18 + import * as checks from './ast/checks'; 18 19 import { 19 20 bubbleUpCallExpression, 20 21 bubbleUpTemplate, 21 22 findNode, 22 23 getAllFragments, 23 - getSchemaName, 24 24 getSource, 25 25 } from './ast'; 26 26 import { Cursor } from './ast/cursor'; 27 27 import { resolveTemplate } from './ast/resolve'; 28 28 import { getToken } from './ast/token'; 29 29 import { getSuggestionsForFragmentSpread } from './graphql/getFragmentSpreadSuggestions'; 30 - import { templates } from './ast/templates'; 31 30 import { SchemaRef } from './graphql/getSchema'; 32 31 33 32 export function getGraphQLCompletions( ··· 37 36 info: ts.server.PluginCreateInfo 38 37 ): ts.WithMetadata<ts.CompletionInfo> | undefined { 39 38 const isCallExpression = info.config.templateIsCallExpression ?? true; 40 - 39 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 41 40 const source = getSource(info, filename); 42 41 if (!source) return undefined; 43 42 ··· 49 48 : bubbleUpTemplate(node); 50 49 51 50 let text, cursor, schemaToUse: GraphQLSchema | undefined; 52 - if ( 53 - ts.isCallExpression(node) && 54 - isCallExpression && 55 - templates.has(node.expression.getText()) && 56 - node.arguments.length > 0 && 57 - ts.isNoSubstitutionTemplateLiteral(node.arguments[0]) 58 - ) { 59 - const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 60 - const schemaName = getSchemaName(node, typeChecker); 51 + if (isCallExpression && checks.isGraphQLCall(node, typeChecker)) { 52 + const schemaName = checks.getSchemaName(node, typeChecker); 61 53 62 54 schemaToUse = 63 55 schemaName && schema.multi[schemaName] ··· 72 64 73 65 text = `${queryText}\n${fragments.map(x => print(x)).join('\n')}`; 74 66 cursor = new Cursor(foundToken.line, foundToken.start - 1); 75 - } else if (ts.isTaggedTemplateExpression(node)) { 76 - const { template, tag } = node; 77 - 78 - if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined; 79 - 80 - const foundToken = getToken(template, cursorPosition); 67 + } else if (!isCallExpression && checks.isGraphQLTag(node)) { 68 + const foundToken = getToken(node.template, cursorPosition); 81 69 if (!foundToken || !schema.current) return undefined; 82 70 83 71 const { combinedText, resolvedSpans } = resolveTemplate(
+3 -3
packages/graphqlsp/src/diagnostics.ts
··· 87 87 88 88 let fragments: Array<FragmentDefinitionNode> = [], 89 89 nodes: { 90 - node: ts.NoSubstitutionTemplateLiteral | ts.TaggedTemplateExpression; 90 + node: ts.StringLiteralLike | ts.TaggedTemplateExpression; 91 91 schema: string | null; 92 92 }[]; 93 93 if (isCallExpression) { ··· 228 228 if ( 229 229 !initializer || 230 230 !ts.isCallExpression(initializer) || 231 - !ts.isNoSubstitutionTemplateLiteral(initializer.arguments[0]) 231 + !ts.isStringLiteralLike(initializer.arguments[0]) 232 232 ) { 233 233 // TODO: we can make this check more stringent where we also parse and resolve 234 234 // the accompanying template. ··· 338 338 fragments, 339 339 }: { 340 340 nodes: { 341 - node: ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral; 341 + node: ts.TaggedTemplateExpression | ts.StringLiteralLike; 342 342 schema: string | null; 343 343 }[]; 344 344 fragments: FragmentDefinitionNode[];
+11 -22
packages/graphqlsp/src/persisted.ts
··· 2 2 3 3 import { createHash } from 'crypto'; 4 4 5 + import * as checks from './ast/checks'; 5 6 import { findAllCallExpressions, findNode, getSource } from './ast'; 6 7 import { resolveTemplate } from './ast/resolve'; 7 - import { templates } from './ast/templates'; 8 8 import { parse, print, visit } from '@0no-co/graphql.web'; 9 9 10 10 type PersistedAction = { ··· 15 15 replacement: string; 16 16 }; 17 17 18 - const isPersistedCall = (expr: ts.LeftHandSideExpression) => { 19 - const expressionText = expr.getText(); 20 - const [template, method] = expressionText.split('.'); 21 - return templates.has(template) && method === 'persisted'; 22 - }; 23 - 24 18 export function getPersistedCodeFixAtPosition( 25 19 filename: string, 26 20 position: number, 27 21 info: ts.server.PluginCreateInfo 28 22 ): PersistedAction | undefined { 29 23 const isCallExpression = info.config.templateIsCallExpression ?? true; 30 - 24 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 31 25 if (!isCallExpression) return undefined; 32 26 33 27 let source = getSource(info, filename); ··· 77 71 // like "graphql.persisted", in a future iteration when the API surface 78 72 // is more defined we will need to use the ts.Symbol to support re-exporting 79 73 // this function by means of "export const peristed = graphql.persisted". 80 - if ( 81 - (callExpression && !ts.isCallExpression(callExpression)) || 82 - !isPersistedCall(callExpression.expression) || 83 - (!callExpression.typeArguments && !callExpression.arguments[1]) 84 - ) 74 + if (!checks.isTadaPersistedCall(callExpression, typeChecker)) { 85 75 return undefined; 76 + } 86 77 87 78 let foundNode, 88 79 foundFilename = filename; ··· 115 106 if ( 116 107 !initializer || 117 108 !ts.isCallExpression(initializer) || 118 - !ts.isNoSubstitutionTemplateLiteral(initializer.arguments[0]) 109 + !ts.isStringLiteralLike(initializer.arguments[0]) 119 110 ) 120 111 return undefined; 121 112 ··· 164 155 165 156 export const generateHashForDocument = ( 166 157 info: ts.server.PluginCreateInfo, 167 - templateLiteral: 168 - | ts.NoSubstitutionTemplateLiteral 169 - | ts.TaggedTemplateExpression, 158 + templateLiteral: ts.StringLiteralLike | ts.TaggedTemplateExpression, 170 159 foundFilename: string 171 160 ): string | undefined => { 172 161 const externalSource = getSource(info, foundFilename)!; ··· 189 178 [...spreads].forEach(spreadName => { 190 179 const fragmentDefinition = fragments.find(x => x.name.value === spreadName); 191 180 if (!fragmentDefinition) { 192 - console.warn( 181 + info.project.projectService.logger.info( 193 182 `[GraphQLSP] could not find fragment for spread ${spreadName}!` 194 183 ); 195 184 return; ··· 215 204 216 205 if (!references) return { node: null, filename }; 217 206 207 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 218 208 let found: ts.CallExpression | null = null; 219 209 let foundFilename = filename; 220 210 references.forEach(ref => { ··· 228 218 if ( 229 219 ts.isVariableDeclaration(foundNode.parent) && 230 220 foundNode.parent.initializer && 231 - ts.isCallExpression(foundNode.parent.initializer) && 232 - templates.has(foundNode.parent.initializer.expression.getText()) 221 + checks.isGraphQLCall(foundNode.parent.initializer, typeChecker) 233 222 ) { 234 223 found = foundNode.parent.initializer; 235 224 foundFilename = ref.fileName; ··· 254 243 255 244 if (!references) return { node: null, filename }; 256 245 246 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 257 247 let found: ts.CallExpression | null = null; 258 248 let foundFilename = filename; 259 249 references.forEach(ref => { ··· 267 257 if ( 268 258 ts.isVariableDeclaration(foundNode.parent) && 269 259 foundNode.parent.initializer && 270 - ts.isCallExpression(foundNode.parent.initializer) && 271 - templates.has(foundNode.parent.initializer.expression.getText()) 260 + checks.isGraphQLCall(foundNode.parent.initializer, typeChecker) 272 261 ) { 273 262 found = foundNode.parent.initializer; 274 263 foundFilename = ref.fileName;
+6 -13
packages/graphqlsp/src/quickInfo.ts
··· 9 9 getSchemaName, 10 10 getSource, 11 11 } from './ast'; 12 + 13 + import * as checks from './ast/checks'; 12 14 import { resolveTemplate } from './ast/resolve'; 13 15 import { getToken } from './ast/token'; 14 16 import { Cursor } from './ast/cursor'; ··· 22 24 info: ts.server.PluginCreateInfo 23 25 ): ts.QuickInfo | undefined { 24 26 const isCallExpression = info.config.templateIsCallExpression ?? true; 27 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 25 28 26 29 const source = getSource(info, filename); 27 30 if (!source) return undefined; ··· 34 37 : bubbleUpTemplate(node); 35 38 36 39 let cursor, text, schemaToUse: GraphQLSchema | undefined; 37 - if ( 38 - ts.isCallExpression(node) && 39 - isCallExpression && 40 - templates.has(node.expression.getText()) && 41 - node.arguments.length > 0 && 42 - ts.isNoSubstitutionTemplateLiteral(node.arguments[0]) 43 - ) { 40 + if (isCallExpression && checks.isGraphQLCall(node, typeChecker)) { 44 41 const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 45 42 const schemaName = getSchemaName(node, typeChecker); 46 43 ··· 54 51 55 52 text = node.arguments[0].getText(); 56 53 cursor = new Cursor(foundToken.line, foundToken.start - 1); 57 - } else if (ts.isTaggedTemplateExpression(node)) { 58 - const { template, tag } = node; 59 - if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined; 60 - 61 - const foundToken = getToken(template, cursorPosition); 62 - 54 + } else if (!isCallExpression && checks.isGraphQLTag(node)) { 55 + const foundToken = getToken(node.template, cursorPosition); 63 56 if (!foundToken || !schema.current) return undefined; 64 57 65 58 const { combinedText, resolvedSpans } = resolveTemplate(