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.

refactor

+287 -237
-3
packages/graphqlsp/src/autoComplete.ts
··· 30 30 import { resolveTemplate } from './ast/resolve'; 31 31 import { getToken } from './ast/token'; 32 32 import { getSuggestionsForFragmentSpread } from './graphql/getFragmentSpreadSuggestions'; 33 - import { Logger } from '.'; 34 33 35 34 export function getGraphQLCompletions( 36 35 filename: string, ··· 38 37 schema: { current: GraphQLSchema | null }, 39 38 info: ts.server.PluginCreateInfo 40 39 ): ts.WithMetadata<ts.CompletionInfo> | undefined { 41 - const logger: Logger = (msg: string) => 42 - info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 43 40 const tagTemplate = info.config.template || 'gql'; 44 41 const isCallExpression = info.config.templateIsCallExpression ?? false; 45 42
+287 -234
packages/graphqlsp/src/diagnostics.ts
··· 19 19 } from './ast'; 20 20 import { resolveTemplate } from './ast/resolve'; 21 21 import { generateTypedDocumentNodes } from './graphql/generateTypes'; 22 - import { Logger } from '.'; 23 22 24 23 export const SEMANTIC_DIAGNOSTIC_CODE = 52001; 25 24 export const MISSING_OPERATION_NAME_CODE = 52002; ··· 37 36 export function getGraphQLDiagnostics( 38 37 // This is so that we don't change offsets when there are 39 38 // TypeScript errors 40 - hasTSErrors: Boolean, 39 + hasTSErrors: boolean, 41 40 filename: string, 42 41 baseTypesPath: string, 43 42 schema: { current: GraphQLSchema | null; version: number }, 44 43 info: ts.server.PluginCreateInfo 45 44 ): ts.Diagnostic[] | undefined { 46 - const logger: Logger = (msg: string) => 47 - info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 48 - const disableTypegen = info.config.disableTypegen ?? false; 49 45 const tagTemplate = info.config.template || 'gql'; 50 - const scalars = info.config.scalars || {}; 51 - const shouldCheckForColocatedFragments = 52 - info.config.shouldCheckForColocatedFragments ?? false; 53 46 const isCallExpression = info.config.templateIsCallExpression ?? false; 54 47 55 48 let source = getSource(info, filename); ··· 86 79 if (cache.has(cacheKey)) { 87 80 tsDiagnostics = cache.get(cacheKey)!; 88 81 } else { 89 - const diagnostics = nodes 90 - .map(originalNode => { 91 - let node = originalNode; 92 - if ( 93 - !isCallExpression && 94 - (ts.isNoSubstitutionTemplateLiteral(node) || 95 - ts.isTemplateExpression(node)) 96 - ) { 97 - if (ts.isTaggedTemplateExpression(node.parent)) { 98 - node = node.parent; 99 - } else { 100 - return undefined; 101 - } 82 + tsDiagnostics = runDiagnostics(source, { nodes, fragments }, schema, info); 83 + cache.set(cacheKey, tsDiagnostics); 84 + } 85 + 86 + runTypedDocumentNodes( 87 + nodes, 88 + texts, 89 + schema, 90 + tsDiagnostics, 91 + hasTSErrors, 92 + baseTypesPath, 93 + source, 94 + info 95 + ); 96 + 97 + return tsDiagnostics; 98 + } 99 + 100 + const runDiagnostics = ( 101 + source: ts.SourceFile, 102 + { 103 + nodes, 104 + fragments, 105 + }: { 106 + nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[]; 107 + fragments: FragmentDefinitionNode[]; 108 + }, 109 + schema: { current: GraphQLSchema | null; version: number }, 110 + info: ts.server.PluginCreateInfo 111 + ) => { 112 + const tagTemplate = info.config.template || 'gql'; 113 + const filename = source.fileName; 114 + const isCallExpression = info.config.templateIsCallExpression ?? false; 115 + 116 + const diagnostics = nodes 117 + .map(originalNode => { 118 + let node = originalNode; 119 + if ( 120 + !isCallExpression && 121 + (ts.isNoSubstitutionTemplateLiteral(node) || 122 + ts.isTemplateExpression(node)) 123 + ) { 124 + if (ts.isTaggedTemplateExpression(node.parent)) { 125 + node = node.parent; 126 + } else { 127 + return undefined; 102 128 } 129 + } 103 130 104 - const { combinedText: text, resolvedSpans } = resolveTemplate( 105 - node, 106 - filename, 107 - info 108 - ); 109 - const lines = text.split('\n'); 131 + const { combinedText: text, resolvedSpans } = resolveTemplate( 132 + node, 133 + filename, 134 + info 135 + ); 136 + const lines = text.split('\n'); 110 137 111 - let isExpression = false; 112 - if (ts.isAsExpression(node.parent)) { 113 - if (ts.isExpressionStatement(node.parent.parent)) { 114 - isExpression = true; 115 - } 116 - } else if (ts.isExpressionStatement(node.parent)) { 138 + let isExpression = false; 139 + if (ts.isAsExpression(node.parent)) { 140 + if (ts.isExpressionStatement(node.parent.parent)) { 117 141 isExpression = true; 118 142 } 119 - // When we are dealing with a plain gql statement we have to add two these can be recognised 120 - // by the fact that the parent is an expressionStatement 121 - let startingPosition = 122 - node.pos + 123 - (isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1)); 124 - const endPosition = startingPosition + node.getText().length; 143 + } else if (ts.isExpressionStatement(node.parent)) { 144 + isExpression = true; 145 + } 146 + // When we are dealing with a plain gql statement we have to add two these can be recognised 147 + // by the fact that the parent is an expressionStatement 148 + let startingPosition = 149 + node.pos + 150 + (isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1)); 151 + const endPosition = startingPosition + node.getText().length; 125 152 126 - let docFragments = [...fragments]; 127 - if (isCallExpression) { 128 - const documentFragments = parse(text, { 129 - noLocation: true, 130 - }).definitions.filter(x => x.kind === Kind.FRAGMENT_DEFINITION); 131 - docFragments = docFragments.filter( 132 - x => 133 - !documentFragments.some( 134 - y => 135 - y.kind === Kind.FRAGMENT_DEFINITION && 136 - y.name.value === x.name.value 137 - ) 138 - ); 139 - } 153 + let docFragments = [...fragments]; 154 + if (isCallExpression) { 155 + const documentFragments = parse(text, { 156 + noLocation: true, 157 + }).definitions.filter(x => x.kind === Kind.FRAGMENT_DEFINITION); 158 + docFragments = docFragments.filter( 159 + x => 160 + !documentFragments.some( 161 + y => 162 + y.kind === Kind.FRAGMENT_DEFINITION && 163 + y.name.value === x.name.value 164 + ) 165 + ); 166 + } 140 167 141 - const graphQLDiagnostics = getDiagnostics( 142 - text, 143 - schema.current, 144 - undefined, 145 - undefined, 146 - docFragments 147 - ) 148 - .map(x => { 149 - const { start, end } = x.range; 168 + const graphQLDiagnostics = getDiagnostics( 169 + text, 170 + schema.current, 171 + undefined, 172 + undefined, 173 + docFragments 174 + ) 175 + .map(x => { 176 + const { start, end } = x.range; 150 177 151 - // We add the start.line to account for newline characters which are 152 - // split out 153 - let startChar = startingPosition + start.line; 154 - for (let i = 0; i <= start.line; i++) { 155 - if (i === start.line) startChar += start.character; 156 - else startChar += lines[i].length; 157 - } 178 + // We add the start.line to account for newline characters which are 179 + // split out 180 + let startChar = startingPosition + start.line; 181 + for (let i = 0; i <= start.line; i++) { 182 + if (i === start.line) startChar += start.character; 183 + else startChar += lines[i].length; 184 + } 158 185 159 - let endChar = startingPosition + end.line; 160 - for (let i = 0; i <= end.line; i++) { 161 - if (i === end.line) endChar += end.character; 162 - else endChar += lines[i].length; 163 - } 186 + let endChar = startingPosition + end.line; 187 + for (let i = 0; i <= end.line; i++) { 188 + if (i === end.line) endChar += end.character; 189 + else endChar += lines[i].length; 190 + } 164 191 165 - const locatedInFragment = resolvedSpans.find(x => { 166 - const newEnd = x.new.start + x.new.length; 167 - return startChar >= x.new.start && endChar <= newEnd; 168 - }); 192 + const locatedInFragment = resolvedSpans.find(x => { 193 + const newEnd = x.new.start + x.new.length; 194 + return startChar >= x.new.start && endChar <= newEnd; 195 + }); 169 196 170 - if (!!locatedInFragment) { 197 + if (!!locatedInFragment) { 198 + return { 199 + ...x, 200 + start: locatedInFragment.original.start, 201 + length: locatedInFragment.original.length, 202 + }; 203 + } else { 204 + if (startChar > endPosition) { 205 + // we have to calculate the added length and fix this 206 + const addedCharacters = resolvedSpans 207 + .filter(x => x.new.start + x.new.length < startChar) 208 + .reduce( 209 + (acc, span) => acc + (span.new.length - span.original.length), 210 + 0 211 + ); 212 + startChar = startChar - addedCharacters; 213 + endChar = endChar - addedCharacters; 171 214 return { 172 215 ...x, 173 - start: locatedInFragment.original.start, 174 - length: locatedInFragment.original.length, 216 + start: startChar + 1, 217 + length: endChar - startChar, 175 218 }; 176 219 } else { 177 - if (startChar > endPosition) { 178 - // we have to calculate the added length and fix this 179 - const addedCharacters = resolvedSpans 180 - .filter(x => x.new.start + x.new.length < startChar) 181 - .reduce( 182 - (acc, span) => 183 - acc + (span.new.length - span.original.length), 184 - 0 185 - ); 186 - startChar = startChar - addedCharacters; 187 - endChar = endChar - addedCharacters; 188 - return { 189 - ...x, 190 - start: startChar + 1, 191 - length: endChar - startChar, 192 - }; 193 - } else { 194 - return { 195 - ...x, 196 - start: startChar + 1, 197 - length: endChar - startChar, 198 - }; 199 - } 220 + return { 221 + ...x, 222 + start: startChar + 1, 223 + length: endChar - startChar, 224 + }; 200 225 } 201 - }) 202 - .filter(x => x.start + x.length <= endPosition); 226 + } 227 + }) 228 + .filter(x => x.start + x.length <= endPosition); 203 229 204 - try { 205 - const parsed = parse(text, { noLocation: true }); 230 + try { 231 + const parsed = parse(text, { noLocation: true }); 206 232 207 - if ( 208 - parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION) 209 - ) { 210 - const op = parsed.definitions.find( 211 - x => x.kind === Kind.OPERATION_DEFINITION 212 - ) as OperationDefinitionNode; 213 - if (!op.name) { 214 - graphQLDiagnostics.push({ 215 - message: 'Operation needs a name for types to be generated.', 216 - start: node.pos, 217 - code: MISSING_OPERATION_NAME_CODE, 218 - length: originalNode.getText().length, 219 - range: {} as any, 220 - severity: 2, 221 - } as any); 222 - } 233 + if ( 234 + parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION) 235 + ) { 236 + const op = parsed.definitions.find( 237 + x => x.kind === Kind.OPERATION_DEFINITION 238 + ) as OperationDefinitionNode; 239 + if (!op.name) { 240 + graphQLDiagnostics.push({ 241 + message: 'Operation needs a name for types to be generated.', 242 + start: node.pos, 243 + code: MISSING_OPERATION_NAME_CODE, 244 + length: originalNode.getText().length, 245 + range: {} as any, 246 + severity: 2, 247 + } as any); 223 248 } 224 - } catch (e) {} 249 + } 250 + } catch (e) {} 225 251 226 - return graphQLDiagnostics; 227 - }) 228 - .flat() 229 - .filter(Boolean) as Array<Diagnostic & { length: number; start: number }>; 252 + return graphQLDiagnostics; 253 + }) 254 + .flat() 255 + .filter(Boolean) as Array<Diagnostic & { length: number; start: number }>; 230 256 231 - tsDiagnostics = diagnostics.map(diag => ({ 232 - file: source, 233 - length: diag.length, 234 - start: diag.start, 235 - category: 236 - diag.severity === 2 237 - ? ts.DiagnosticCategory.Warning 238 - : ts.DiagnosticCategory.Error, 239 - code: 240 - typeof diag.code === 'number' 241 - ? diag.code 242 - : diag.severity === 2 243 - ? USING_DEPRECATED_FIELD_CODE 244 - : SEMANTIC_DIAGNOSTIC_CODE, 245 - messageText: diag.message.split('\n')[0], 246 - })); 257 + const tsDiagnostics = diagnostics.map(diag => ({ 258 + file: source, 259 + length: diag.length, 260 + start: diag.start, 261 + category: 262 + diag.severity === 2 263 + ? ts.DiagnosticCategory.Warning 264 + : ts.DiagnosticCategory.Error, 265 + code: 266 + typeof diag.code === 'number' 267 + ? diag.code 268 + : diag.severity === 2 269 + ? USING_DEPRECATED_FIELD_CODE 270 + : SEMANTIC_DIAGNOSTIC_CODE, 271 + messageText: diag.message.split('\n')[0], 272 + })); 247 273 248 - const imports = findAllImports(source); 249 - if (imports.length && shouldCheckForColocatedFragments) { 250 - const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 251 - imports.forEach(imp => { 252 - if (!imp.importClause) return; 274 + const importDiagnostics = checkImportsForFragments(source, info); 253 275 254 - const importedNames: string[] = []; 255 - if (imp.importClause.name) { 256 - importedNames.push(imp.importClause?.name.text); 257 - } 276 + return [...tsDiagnostics, ...importDiagnostics]; 277 + }; 258 278 259 - if ( 260 - imp.importClause.namedBindings && 261 - ts.isNamespaceImport(imp.importClause.namedBindings) 262 - ) { 263 - // TODO: we might need to warn here when the fragment is unused as a namespace import 264 - return; 265 - } else if ( 266 - imp.importClause.namedBindings && 267 - ts.isNamedImportBindings(imp.importClause.namedBindings) 268 - ) { 269 - imp.importClause.namedBindings.elements.forEach(el => { 270 - importedNames.push(el.name.text); 271 - }); 272 - } 279 + const checkImportsForFragments = ( 280 + source: ts.SourceFile, 281 + info: ts.server.PluginCreateInfo 282 + ) => { 283 + const imports = findAllImports(source); 273 284 274 - const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier); 275 - if (!symbol) return; 285 + const shouldCheckForColocatedFragments = 286 + info.config.shouldCheckForColocatedFragments ?? false; 287 + const tsDiagnostics: ts.Diagnostic[] = []; 288 + if (imports.length && shouldCheckForColocatedFragments) { 289 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 290 + imports.forEach(imp => { 291 + if (!imp.importClause) return; 276 292 277 - const moduleExports = typeChecker?.getExportsOfModule(symbol); 278 - if (!moduleExports) return; 293 + const importedNames: string[] = []; 294 + if (imp.importClause.name) { 295 + importedNames.push(imp.importClause?.name.text); 296 + } 279 297 280 - const missingImports = moduleExports 281 - .map(exp => { 282 - if (importedNames.includes(exp.name)) { 283 - return; 284 - } 298 + if ( 299 + imp.importClause.namedBindings && 300 + ts.isNamespaceImport(imp.importClause.namedBindings) 301 + ) { 302 + // TODO: we might need to warn here when the fragment is unused as a namespace import 303 + return; 304 + } else if ( 305 + imp.importClause.namedBindings && 306 + ts.isNamedImportBindings(imp.importClause.namedBindings) 307 + ) { 308 + imp.importClause.namedBindings.elements.forEach(el => { 309 + importedNames.push(el.name.text); 310 + }); 311 + } 285 312 286 - const declarations = exp.getDeclarations(); 287 - const declaration = declarations?.find(x => { 288 - // TODO: check whether the sourceFile.fileName resembles the module 289 - // specifier 290 - return true; 291 - }); 313 + const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier); 314 + if (!symbol) return; 292 315 293 - if (!declaration) return; 316 + const moduleExports = typeChecker?.getExportsOfModule(symbol); 317 + if (!moduleExports) return; 294 318 295 - const [template] = findAllTaggedTemplateNodes(declaration); 296 - if (template) { 297 - let node = template; 298 - if ( 299 - ts.isNoSubstitutionTemplateLiteral(node) || 300 - ts.isTemplateExpression(node) 301 - ) { 302 - if (ts.isTaggedTemplateExpression(node.parent)) { 303 - node = node.parent; 304 - } else { 305 - return; 306 - } 307 - } 319 + const missingImports = moduleExports 320 + .map(exp => { 321 + if (importedNames.includes(exp.name)) { 322 + return; 323 + } 308 324 309 - const text = resolveTemplate( 310 - node, 311 - node.getSourceFile().fileName, 312 - info 313 - ).combinedText; 314 - try { 315 - const parsed = parse(text, { noLocation: true }); 316 - if ( 317 - parsed.definitions.every( 318 - x => x.kind === Kind.FRAGMENT_DEFINITION 319 - ) 320 - ) { 321 - return `'${exp.name}'`; 322 - } 323 - } catch (e) { 325 + const declarations = exp.getDeclarations(); 326 + const declaration = declarations?.find(x => { 327 + // TODO: check whether the sourceFile.fileName resembles the module 328 + // specifier 329 + return true; 330 + }); 331 + 332 + if (!declaration) return; 333 + 334 + const [template] = findAllTaggedTemplateNodes(declaration); 335 + if (template) { 336 + let node = template; 337 + if ( 338 + ts.isNoSubstitutionTemplateLiteral(node) || 339 + ts.isTemplateExpression(node) 340 + ) { 341 + if (ts.isTaggedTemplateExpression(node.parent)) { 342 + node = node.parent; 343 + } else { 324 344 return; 325 345 } 326 346 } 327 - }) 328 - .filter(Boolean); 329 347 330 - if (missingImports.length) { 331 - tsDiagnostics.push({ 332 - file: source, 333 - length: imp.getText().length, 334 - start: imp.getStart(), 335 - category: ts.DiagnosticCategory.Message, 336 - code: MISSING_FRAGMENT_CODE, 337 - messageText: `Missing Fragment import(s) ${missingImports.join( 338 - ', ' 339 - )} from ${imp.moduleSpecifier.getText()}.`, 340 - }); 341 - } 342 - }); 343 - } 348 + const text = resolveTemplate( 349 + node, 350 + node.getSourceFile().fileName, 351 + info 352 + ).combinedText; 353 + try { 354 + const parsed = parse(text, { noLocation: true }); 355 + if ( 356 + parsed.definitions.every( 357 + x => x.kind === Kind.FRAGMENT_DEFINITION 358 + ) 359 + ) { 360 + return `'${exp.name}'`; 361 + } 362 + } catch (e) { 363 + return; 364 + } 365 + } 366 + }) 367 + .filter(Boolean); 344 368 345 - cache.set(cacheKey, tsDiagnostics); 369 + if (missingImports.length) { 370 + tsDiagnostics.push({ 371 + file: source, 372 + length: imp.getText().length, 373 + start: imp.getStart(), 374 + category: ts.DiagnosticCategory.Message, 375 + code: MISSING_FRAGMENT_CODE, 376 + messageText: `Missing Fragment import(s) ${missingImports.join( 377 + ', ' 378 + )} from ${imp.moduleSpecifier.getText()}.`, 379 + }); 380 + } 381 + }); 346 382 } 347 383 384 + return tsDiagnostics; 385 + }; 386 + 387 + const runTypedDocumentNodes = ( 388 + nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[], 389 + texts: (string | undefined)[], 390 + schema: { current: GraphQLSchema | null }, 391 + diagnostics: ts.Diagnostic[], 392 + hasTSErrors: boolean, 393 + baseTypesPath: string, 394 + sourceFile: ts.SourceFile, 395 + info: ts.server.PluginCreateInfo 396 + ) => { 397 + const filename = sourceFile.fileName; 398 + const scalars = info.config.scalars || {}; 399 + const disableTypegen = info.config.disableTypegen ?? false; 400 + let source: ts.SourceFile | undefined = sourceFile; 401 + 348 402 if ( 349 - !tsDiagnostics.filter( 403 + !diagnostics.filter( 350 404 x => 351 405 x.category === ts.DiagnosticCategory.Error || 352 406 x.category === ts.DiagnosticCategory.Warning ··· 355 409 ) { 356 410 try { 357 411 if (isFileDirty(filename, source) && !isGeneratingTypes) { 358 - return tsDiagnostics; 412 + return; 359 413 } 414 + 360 415 isGeneratingTypes = true; 361 416 362 417 const parts = source.fileName.split('/'); ··· 474 529 isGeneratingTypes = false; 475 530 } 476 531 } 477 - 478 - return tsDiagnostics; 479 - } 532 + };