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.

cleanup

+411 -404
+115
packages/graphqlsp/src/checkImports.ts
··· 1 + import ts from 'typescript/lib/tsserverlibrary'; 2 + import { Kind, parse } from 'graphql'; 3 + 4 + import { findAllImports, findAllTaggedTemplateNodes } from './ast'; 5 + import { resolveTemplate } from './ast/resolve'; 6 + 7 + export const MISSING_FRAGMENT_CODE = 52003; 8 + 9 + export const checkImportsForFragments = ( 10 + source: ts.SourceFile, 11 + info: ts.server.PluginCreateInfo 12 + ) => { 13 + const imports = findAllImports(source); 14 + 15 + const shouldCheckForColocatedFragments = 16 + info.config.shouldCheckForColocatedFragments ?? false; 17 + const tsDiagnostics: ts.Diagnostic[] = []; 18 + if (imports.length && shouldCheckForColocatedFragments) { 19 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 20 + imports.forEach(imp => { 21 + if (!imp.importClause) return; 22 + 23 + const importedNames: string[] = []; 24 + if (imp.importClause.name) { 25 + importedNames.push(imp.importClause?.name.text); 26 + } 27 + 28 + if ( 29 + imp.importClause.namedBindings && 30 + ts.isNamespaceImport(imp.importClause.namedBindings) 31 + ) { 32 + // TODO: we might need to warn here when the fragment is unused as a namespace import 33 + return; 34 + } else if ( 35 + imp.importClause.namedBindings && 36 + ts.isNamedImportBindings(imp.importClause.namedBindings) 37 + ) { 38 + imp.importClause.namedBindings.elements.forEach(el => { 39 + importedNames.push(el.name.text); 40 + }); 41 + } 42 + 43 + const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier); 44 + if (!symbol) return; 45 + 46 + const moduleExports = typeChecker?.getExportsOfModule(symbol); 47 + if (!moduleExports) return; 48 + 49 + const missingImports = moduleExports 50 + .map(exp => { 51 + if (importedNames.includes(exp.name)) { 52 + return; 53 + } 54 + 55 + const declarations = exp.getDeclarations(); 56 + const declaration = declarations?.find(x => { 57 + // TODO: check whether the sourceFile.fileName resembles the module 58 + // specifier 59 + return true; 60 + }); 61 + 62 + if (!declaration) return; 63 + 64 + const [template] = findAllTaggedTemplateNodes(declaration); 65 + if (template) { 66 + let node = template; 67 + if ( 68 + ts.isNoSubstitutionTemplateLiteral(node) || 69 + ts.isTemplateExpression(node) 70 + ) { 71 + if (ts.isTaggedTemplateExpression(node.parent)) { 72 + node = node.parent; 73 + } else { 74 + return; 75 + } 76 + } 77 + 78 + const text = resolveTemplate( 79 + node, 80 + node.getSourceFile().fileName, 81 + info 82 + ).combinedText; 83 + try { 84 + const parsed = parse(text, { noLocation: true }); 85 + if ( 86 + parsed.definitions.every( 87 + x => x.kind === Kind.FRAGMENT_DEFINITION 88 + ) 89 + ) { 90 + return `'${exp.name}'`; 91 + } 92 + } catch (e) { 93 + return; 94 + } 95 + } 96 + }) 97 + .filter(Boolean); 98 + 99 + if (missingImports.length) { 100 + tsDiagnostics.push({ 101 + file: source, 102 + length: imp.getText().length, 103 + start: imp.getStart(), 104 + category: ts.DiagnosticCategory.Message, 105 + code: MISSING_FRAGMENT_CODE, 106 + messageText: `Missing Fragment import(s) ${missingImports.join( 107 + ', ' 108 + )} from ${imp.moduleSpecifier.getText()}.`, 109 + }); 110 + } 111 + }); 112 + } 113 + 114 + return tsDiagnostics; 115 + };
+2 -404
packages/graphqlsp/src/diagnostics.ts
··· 7 7 OperationDefinitionNode, 8 8 parse, 9 9 print, 10 - visit, 11 10 } from 'graphql'; 12 11 import { LRUCache } from 'lru-cache'; 13 12 import fnv1a from '@sindresorhus/fnv1a'; 14 13 15 14 import { 16 15 findAllCallExpressions, 17 - findAllImports, 18 16 findAllTaggedTemplateNodes, 19 - findNode, 20 17 getSource, 21 18 isFileDirty, 22 19 } from './ast'; 23 20 import { resolveTemplate } from './ast/resolve'; 24 21 import { generateTypedDocumentNodes } from './graphql/generateTypes'; 25 - import { Logger } from '.'; 22 + import { checkFieldUsageInFile } from './fieldUsage'; 23 + import { checkImportsForFragments } from './checkImports'; 26 24 27 25 const clientDirectives = new Set([ 28 26 'populate', ··· 41 39 42 40 export const SEMANTIC_DIAGNOSTIC_CODE = 52001; 43 41 export const MISSING_OPERATION_NAME_CODE = 52002; 44 - export const MISSING_FRAGMENT_CODE = 52003; 45 42 export const USING_DEPRECATED_FIELD_CODE = 52004; 46 - export const UNUSED_FIELD_CODE = 52005; 47 43 48 44 let isGeneratingTypes = false; 49 45 ··· 317 313 : checkImportsForFragments(source, info); 318 314 319 315 return [...tsDiagnostics, ...importDiagnostics]; 320 - }; 321 - 322 - const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => { 323 - let node: any = start; 324 - let counter = 0; 325 - while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) { 326 - node = node.parent; 327 - counter++; 328 - } 329 - return node; 330 - }; 331 - 332 - const traverseDestructuring = ( 333 - node: ts.ObjectBindingPattern, 334 - originalWip: Array<string>, 335 - allFields: Array<string>, 336 - source: ts.SourceFile, 337 - info: ts.server.PluginCreateInfo 338 - ): Array<string> => { 339 - const results = []; 340 - for (const binding of node.elements) { 341 - if (ts.isObjectBindingPattern(binding.name)) { 342 - const wip = [...originalWip]; 343 - if ( 344 - binding.propertyName && 345 - allFields.includes(binding.propertyName.getText()) && 346 - !originalWip.includes(binding.propertyName.getText()) 347 - ) { 348 - wip.push(binding.propertyName.getText()); 349 - } 350 - const traverseResult = traverseDestructuring( 351 - binding.name, 352 - wip, 353 - allFields, 354 - source, 355 - info 356 - ); 357 - 358 - results.push(...traverseResult); 359 - } else if (ts.isIdentifier(binding.name)) { 360 - const wip = [...originalWip]; 361 - if ( 362 - binding.propertyName && 363 - allFields.includes(binding.propertyName.getText()) && 364 - !originalWip.includes(binding.propertyName.getText()) 365 - ) { 366 - wip.push(binding.propertyName.getText()); 367 - } else { 368 - wip.push(binding.name.getText()); 369 - } 370 - 371 - const crawlResult = crawlScope( 372 - binding.name, 373 - wip, 374 - allFields, 375 - source, 376 - info 377 - ); 378 - 379 - results.push(...crawlResult); 380 - } 381 - } 382 - 383 - return results; 384 - }; 385 - 386 - const crawlScope = ( 387 - node: ts.Identifier | ts.BindingName, 388 - originalWip: Array<string>, 389 - allFields: Array<string>, 390 - source: ts.SourceFile, 391 - info: ts.server.PluginCreateInfo 392 - ): Array<string> => { 393 - let results: string[] = []; 394 - 395 - const references = info.languageService.getReferencesAtPosition( 396 - source.fileName, 397 - node.getStart() 398 - ); 399 - 400 - if (!references) return results; 401 - 402 - // Go over all the references tied to the result of 403 - // accessing our equery and collect them as fully 404 - // qualified paths (ideally ending in a leaf-node) 405 - results = references.flatMap(ref => { 406 - // If we get a reference to a different file we can bail 407 - if (ref.fileName !== source.fileName) return []; 408 - // We don't want to end back at our document so we narrow 409 - // the scope. 410 - if ( 411 - node.getStart() <= ref.textSpan.start && 412 - node.getEnd() >= ref.textSpan.start + ref.textSpan.length 413 - ) 414 - return []; 415 - 416 - let foundRef = findNode(source, ref.textSpan.start); 417 - if (!foundRef) return []; 418 - 419 - const pathParts = [...originalWip]; 420 - // In here we'll start crawling all the accessors of result 421 - // and try to determine the total path 422 - // - result.data.pokemon.name --> pokemon.name this is the easy route and never accesses 423 - // any of the recursive functions 424 - // - const pokemon = result.data.pokemon --> this initiates a new crawl with a renewed scope 425 - // - const { pokemon } = result.data --> this initiates a destructuring traversal which will 426 - // either end up in more destructuring traversals or a scope crawl 427 - while ( 428 - ts.isIdentifier(foundRef) || 429 - ts.isPropertyAccessExpression(foundRef) || 430 - ts.isElementAccessExpression(foundRef) || 431 - ts.isVariableDeclaration(foundRef) || 432 - ts.isBinaryExpression(foundRef) 433 - ) { 434 - if (ts.isVariableDeclaration(foundRef)) { 435 - if (ts.isIdentifier(foundRef.name)) { 436 - // We have already added the paths because of the right-hand expression, 437 - // const pokemon = result.data.pokemon --> we have pokemon as our path, 438 - // now re-crawling pokemon for all of its accessors should deliver us the usage 439 - // patterns... This might get expensive though if we need to perform this deeply. 440 - return crawlScope(foundRef.name, pathParts, allFields, source, info); 441 - } else if (ts.isObjectBindingPattern(foundRef.name)) { 442 - // First we need to traverse the left-hand side of the variable assignment, 443 - // this could be tree-like as we could be dealing with 444 - // - const { x: { y: z }, a: { b: { c, d }, e: { f } } } = result.data 445 - // Which we will need several paths for... 446 - // after doing that we need to re-crawl all of the resulting variables 447 - // Crawl down until we have either a leaf node or an object/array that can 448 - // be recrawled 449 - return traverseDestructuring( 450 - foundRef.name, 451 - pathParts, 452 - allFields, 453 - source, 454 - info 455 - ); 456 - } 457 - } else if ( 458 - ts.isIdentifier(foundRef) && 459 - allFields.includes(foundRef.text) && 460 - !pathParts.includes(foundRef.text) 461 - ) { 462 - pathParts.push(foundRef.text); 463 - } else if ( 464 - ts.isPropertyAccessExpression(foundRef) && 465 - allFields.includes(foundRef.name.text) && 466 - !pathParts.includes(foundRef.name.text) 467 - ) { 468 - pathParts.push(foundRef.name.text); 469 - } else if ( 470 - ts.isElementAccessExpression(foundRef) && 471 - ts.isStringLiteral(foundRef.argumentExpression) && 472 - allFields.includes(foundRef.argumentExpression.text) && 473 - !pathParts.includes(foundRef.argumentExpression.text) 474 - ) { 475 - pathParts.push(foundRef.argumentExpression.text); 476 - } 477 - 478 - foundRef = foundRef.parent; 479 - } 480 - 481 - return pathParts.join('.'); 482 - }); 483 - 484 - return results; 485 - }; 486 - 487 - const checkFieldUsageInFile = ( 488 - source: ts.SourceFile, 489 - nodes: ts.NoSubstitutionTemplateLiteral[], 490 - info: ts.server.PluginCreateInfo 491 - ) => { 492 - const logger: Logger = (msg: string) => 493 - info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 494 - const diagnostics: ts.Diagnostic[] = []; 495 - const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false; 496 - if (!shouldTrackFieldUsage) return diagnostics; 497 - 498 - nodes.forEach(node => { 499 - const nodeText = node.getText(); 500 - // Bailing for mutations/subscriptions as these could have small details 501 - // for normalised cache interactions 502 - if (nodeText.includes('mutation') || nodeText.includes('subscription')) 503 - return; 504 - 505 - const variableDeclaration = getVariableDeclaration(node); 506 - if (!ts.isVariableDeclaration(variableDeclaration)) return; 507 - 508 - const references = info.languageService.getReferencesAtPosition( 509 - source.fileName, 510 - variableDeclaration.name.getStart() 511 - ); 512 - if (!references) return; 513 - 514 - references.forEach(ref => { 515 - if (ref.fileName !== source.fileName) return; 516 - 517 - let found = findNode(source, ref.textSpan.start); 518 - while (found && !ts.isVariableStatement(found)) { 519 - found = found.parent; 520 - } 521 - 522 - if (!found || !ts.isVariableStatement(found)) return; 523 - 524 - const [output] = found.declarationList.declarations; 525 - 526 - if (output.name.getText() === variableDeclaration.name.getText()) return; 527 - 528 - const inProgress: string[] = []; 529 - const allPaths: string[] = []; 530 - const allFields: string[] = []; 531 - const reserved = ['id', '__typename']; 532 - const fieldToLoc = new Map<string, { start: number; length: number }>(); 533 - // This visitor gets all the leaf-paths in the document 534 - // as well as all fields that are part of the document 535 - // We need the leaf-paths to check usage and we need the 536 - // fields to validate whether an access on a given reference 537 - // is valid given the current document... 538 - visit(parse(node.getText().slice(1, -1)), { 539 - Field: { 540 - enter: node => { 541 - if (!reserved.includes(node.name.value)) { 542 - allFields.push(node.name.value); 543 - } 544 - 545 - if (!node.selectionSet && !reserved.includes(node.name.value)) { 546 - let p; 547 - if (inProgress.length) { 548 - p = inProgress.join('.') + '.' + node.name.value; 549 - } else { 550 - p = node.name.value; 551 - } 552 - allPaths.push(p); 553 - 554 - fieldToLoc.set(p, { 555 - start: node.name.loc!.start, 556 - length: node.name.loc!.end - node.name.loc!.start, 557 - }); 558 - } else if (node.selectionSet) { 559 - inProgress.push(node.name.value); 560 - } 561 - }, 562 - leave: node => { 563 - if (node.selectionSet) { 564 - inProgress.pop(); 565 - } 566 - }, 567 - }, 568 - }); 569 - 570 - let temp = output.name; 571 - // Supported cases: 572 - // - const result = await client.query() || useFragment() 573 - // - const [result] = useQuery() --> urql 574 - // - const { data } = useQuery() --> Apollo 575 - // - const { field } = useFragment() 576 - // - const [{ data }] = useQuery() 577 - // - const { data: { pokemon } } = useQuery() 578 - if ( 579 - ts.isArrayBindingPattern(temp) && 580 - ts.isBindingElement(temp.elements[0]) 581 - ) { 582 - temp = temp.elements[0].name; 583 - } 584 - 585 - let allAccess: string[] = []; 586 - if (ts.isObjectBindingPattern(temp)) { 587 - allAccess = traverseDestructuring(temp, [], allFields, source, info); 588 - } else { 589 - allAccess = crawlScope(temp, [], allFields, source, info); 590 - } 591 - 592 - const unused = allPaths.filter(x => !allAccess.includes(x)); 593 - unused.forEach(unusedField => { 594 - const loc = fieldToLoc.get(unusedField); 595 - if (!loc) return; 596 - 597 - diagnostics.push({ 598 - file: source, 599 - length: loc.length, 600 - start: node.getStart() + loc.start + 1, 601 - category: ts.DiagnosticCategory.Warning, 602 - code: UNUSED_FIELD_CODE, 603 - messageText: `Field '${unusedField}' is not used.`, 604 - }); 605 - }); 606 - }); 607 - }); 608 - 609 - return diagnostics; 610 - }; 611 - 612 - const checkImportsForFragments = ( 613 - source: ts.SourceFile, 614 - info: ts.server.PluginCreateInfo 615 - ) => { 616 - const imports = findAllImports(source); 617 - 618 - const shouldCheckForColocatedFragments = 619 - info.config.shouldCheckForColocatedFragments ?? false; 620 - const tsDiagnostics: ts.Diagnostic[] = []; 621 - if (imports.length && shouldCheckForColocatedFragments) { 622 - const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 623 - imports.forEach(imp => { 624 - if (!imp.importClause) return; 625 - 626 - const importedNames: string[] = []; 627 - if (imp.importClause.name) { 628 - importedNames.push(imp.importClause?.name.text); 629 - } 630 - 631 - if ( 632 - imp.importClause.namedBindings && 633 - ts.isNamespaceImport(imp.importClause.namedBindings) 634 - ) { 635 - // TODO: we might need to warn here when the fragment is unused as a namespace import 636 - return; 637 - } else if ( 638 - imp.importClause.namedBindings && 639 - ts.isNamedImportBindings(imp.importClause.namedBindings) 640 - ) { 641 - imp.importClause.namedBindings.elements.forEach(el => { 642 - importedNames.push(el.name.text); 643 - }); 644 - } 645 - 646 - const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier); 647 - if (!symbol) return; 648 - 649 - const moduleExports = typeChecker?.getExportsOfModule(symbol); 650 - if (!moduleExports) return; 651 - 652 - const missingImports = moduleExports 653 - .map(exp => { 654 - if (importedNames.includes(exp.name)) { 655 - return; 656 - } 657 - 658 - const declarations = exp.getDeclarations(); 659 - const declaration = declarations?.find(x => { 660 - // TODO: check whether the sourceFile.fileName resembles the module 661 - // specifier 662 - return true; 663 - }); 664 - 665 - if (!declaration) return; 666 - 667 - const [template] = findAllTaggedTemplateNodes(declaration); 668 - if (template) { 669 - let node = template; 670 - if ( 671 - ts.isNoSubstitutionTemplateLiteral(node) || 672 - ts.isTemplateExpression(node) 673 - ) { 674 - if (ts.isTaggedTemplateExpression(node.parent)) { 675 - node = node.parent; 676 - } else { 677 - return; 678 - } 679 - } 680 - 681 - const text = resolveTemplate( 682 - node, 683 - node.getSourceFile().fileName, 684 - info 685 - ).combinedText; 686 - try { 687 - const parsed = parse(text, { noLocation: true }); 688 - if ( 689 - parsed.definitions.every( 690 - x => x.kind === Kind.FRAGMENT_DEFINITION 691 - ) 692 - ) { 693 - return `'${exp.name}'`; 694 - } 695 - } catch (e) { 696 - return; 697 - } 698 - } 699 - }) 700 - .filter(Boolean); 701 - 702 - if (missingImports.length) { 703 - tsDiagnostics.push({ 704 - file: source, 705 - length: imp.getText().length, 706 - start: imp.getStart(), 707 - category: ts.DiagnosticCategory.Message, 708 - code: MISSING_FRAGMENT_CODE, 709 - messageText: `Missing Fragment import(s) ${missingImports.join( 710 - ', ' 711 - )} from ${imp.moduleSpecifier.getText()}.`, 712 - }); 713 - } 714 - }); 715 - } 716 - 717 - return tsDiagnostics; 718 316 }; 719 317 720 318 const runTypedDocumentNodes = (
+294
packages/graphqlsp/src/fieldUsage.ts
··· 1 + import ts from 'typescript/lib/tsserverlibrary'; 2 + import { parse, visit } from 'graphql'; 3 + 4 + import { findNode } from './ast'; 5 + 6 + export const UNUSED_FIELD_CODE = 52005; 7 + 8 + const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => { 9 + let node: any = start; 10 + let counter = 0; 11 + while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) { 12 + node = node.parent; 13 + counter++; 14 + } 15 + return node; 16 + }; 17 + 18 + const traverseDestructuring = ( 19 + node: ts.ObjectBindingPattern, 20 + originalWip: Array<string>, 21 + allFields: Array<string>, 22 + source: ts.SourceFile, 23 + info: ts.server.PluginCreateInfo 24 + ): Array<string> => { 25 + const results = []; 26 + for (const binding of node.elements) { 27 + if (ts.isObjectBindingPattern(binding.name)) { 28 + const wip = [...originalWip]; 29 + if ( 30 + binding.propertyName && 31 + allFields.includes(binding.propertyName.getText()) && 32 + !originalWip.includes(binding.propertyName.getText()) 33 + ) { 34 + wip.push(binding.propertyName.getText()); 35 + } 36 + const traverseResult = traverseDestructuring( 37 + binding.name, 38 + wip, 39 + allFields, 40 + source, 41 + info 42 + ); 43 + 44 + results.push(...traverseResult); 45 + } else if (ts.isIdentifier(binding.name)) { 46 + const wip = [...originalWip]; 47 + if ( 48 + binding.propertyName && 49 + allFields.includes(binding.propertyName.getText()) && 50 + !originalWip.includes(binding.propertyName.getText()) 51 + ) { 52 + wip.push(binding.propertyName.getText()); 53 + } else { 54 + wip.push(binding.name.getText()); 55 + } 56 + 57 + const crawlResult = crawlScope( 58 + binding.name, 59 + wip, 60 + allFields, 61 + source, 62 + info 63 + ); 64 + 65 + results.push(...crawlResult); 66 + } 67 + } 68 + 69 + return results; 70 + }; 71 + 72 + const crawlScope = ( 73 + node: ts.Identifier | ts.BindingName, 74 + originalWip: Array<string>, 75 + allFields: Array<string>, 76 + source: ts.SourceFile, 77 + info: ts.server.PluginCreateInfo 78 + ): Array<string> => { 79 + let results: string[] = []; 80 + 81 + const references = info.languageService.getReferencesAtPosition( 82 + source.fileName, 83 + node.getStart() 84 + ); 85 + 86 + if (!references) return results; 87 + 88 + // Go over all the references tied to the result of 89 + // accessing our equery and collect them as fully 90 + // qualified paths (ideally ending in a leaf-node) 91 + results = references.flatMap(ref => { 92 + // If we get a reference to a different file we can bail 93 + if (ref.fileName !== source.fileName) return []; 94 + // We don't want to end back at our document so we narrow 95 + // the scope. 96 + if ( 97 + node.getStart() <= ref.textSpan.start && 98 + node.getEnd() >= ref.textSpan.start + ref.textSpan.length 99 + ) 100 + return []; 101 + 102 + let foundRef = findNode(source, ref.textSpan.start); 103 + if (!foundRef) return []; 104 + 105 + const pathParts = [...originalWip]; 106 + // In here we'll start crawling all the accessors of result 107 + // and try to determine the total path 108 + // - result.data.pokemon.name --> pokemon.name this is the easy route and never accesses 109 + // any of the recursive functions 110 + // - const pokemon = result.data.pokemon --> this initiates a new crawl with a renewed scope 111 + // - const { pokemon } = result.data --> this initiates a destructuring traversal which will 112 + // either end up in more destructuring traversals or a scope crawl 113 + while ( 114 + ts.isIdentifier(foundRef) || 115 + ts.isPropertyAccessExpression(foundRef) || 116 + ts.isElementAccessExpression(foundRef) || 117 + ts.isVariableDeclaration(foundRef) || 118 + ts.isBinaryExpression(foundRef) 119 + ) { 120 + if (ts.isVariableDeclaration(foundRef)) { 121 + if (ts.isIdentifier(foundRef.name)) { 122 + // We have already added the paths because of the right-hand expression, 123 + // const pokemon = result.data.pokemon --> we have pokemon as our path, 124 + // now re-crawling pokemon for all of its accessors should deliver us the usage 125 + // patterns... This might get expensive though if we need to perform this deeply. 126 + return crawlScope(foundRef.name, pathParts, allFields, source, info); 127 + } else if (ts.isObjectBindingPattern(foundRef.name)) { 128 + // First we need to traverse the left-hand side of the variable assignment, 129 + // this could be tree-like as we could be dealing with 130 + // - const { x: { y: z }, a: { b: { c, d }, e: { f } } } = result.data 131 + // Which we will need several paths for... 132 + // after doing that we need to re-crawl all of the resulting variables 133 + // Crawl down until we have either a leaf node or an object/array that can 134 + // be recrawled 135 + return traverseDestructuring( 136 + foundRef.name, 137 + pathParts, 138 + allFields, 139 + source, 140 + info 141 + ); 142 + } 143 + } else if ( 144 + ts.isIdentifier(foundRef) && 145 + allFields.includes(foundRef.text) && 146 + !pathParts.includes(foundRef.text) 147 + ) { 148 + pathParts.push(foundRef.text); 149 + } else if ( 150 + ts.isPropertyAccessExpression(foundRef) && 151 + allFields.includes(foundRef.name.text) && 152 + !pathParts.includes(foundRef.name.text) 153 + ) { 154 + pathParts.push(foundRef.name.text); 155 + } else if ( 156 + ts.isElementAccessExpression(foundRef) && 157 + ts.isStringLiteral(foundRef.argumentExpression) && 158 + allFields.includes(foundRef.argumentExpression.text) && 159 + !pathParts.includes(foundRef.argumentExpression.text) 160 + ) { 161 + pathParts.push(foundRef.argumentExpression.text); 162 + } 163 + 164 + foundRef = foundRef.parent; 165 + } 166 + 167 + return pathParts.join('.'); 168 + }); 169 + 170 + return results; 171 + }; 172 + 173 + export const checkFieldUsageInFile = ( 174 + source: ts.SourceFile, 175 + nodes: ts.NoSubstitutionTemplateLiteral[], 176 + info: ts.server.PluginCreateInfo 177 + ) => { 178 + const diagnostics: ts.Diagnostic[] = []; 179 + const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false; 180 + if (!shouldTrackFieldUsage) return diagnostics; 181 + 182 + nodes.forEach(node => { 183 + const nodeText = node.getText(); 184 + // Bailing for mutations/subscriptions as these could have small details 185 + // for normalised cache interactions 186 + if (nodeText.includes('mutation') || nodeText.includes('subscription')) 187 + return; 188 + 189 + const variableDeclaration = getVariableDeclaration(node); 190 + if (!ts.isVariableDeclaration(variableDeclaration)) return; 191 + 192 + const references = info.languageService.getReferencesAtPosition( 193 + source.fileName, 194 + variableDeclaration.name.getStart() 195 + ); 196 + if (!references) return; 197 + 198 + references.forEach(ref => { 199 + if (ref.fileName !== source.fileName) return; 200 + 201 + let found = findNode(source, ref.textSpan.start); 202 + while (found && !ts.isVariableStatement(found)) { 203 + found = found.parent; 204 + } 205 + 206 + if (!found || !ts.isVariableStatement(found)) return; 207 + 208 + const [output] = found.declarationList.declarations; 209 + 210 + if (output.name.getText() === variableDeclaration.name.getText()) return; 211 + 212 + const inProgress: string[] = []; 213 + const allPaths: string[] = []; 214 + const allFields: string[] = []; 215 + const reserved = ['id', '__typename']; 216 + const fieldToLoc = new Map<string, { start: number; length: number }>(); 217 + // This visitor gets all the leaf-paths in the document 218 + // as well as all fields that are part of the document 219 + // We need the leaf-paths to check usage and we need the 220 + // fields to validate whether an access on a given reference 221 + // is valid given the current document... 222 + visit(parse(node.getText().slice(1, -1)), { 223 + Field: { 224 + enter: node => { 225 + if (!reserved.includes(node.name.value)) { 226 + allFields.push(node.name.value); 227 + } 228 + 229 + if (!node.selectionSet && !reserved.includes(node.name.value)) { 230 + let p; 231 + if (inProgress.length) { 232 + p = inProgress.join('.') + '.' + node.name.value; 233 + } else { 234 + p = node.name.value; 235 + } 236 + allPaths.push(p); 237 + 238 + fieldToLoc.set(p, { 239 + start: node.name.loc!.start, 240 + length: node.name.loc!.end - node.name.loc!.start, 241 + }); 242 + } else if (node.selectionSet) { 243 + inProgress.push(node.name.value); 244 + } 245 + }, 246 + leave: node => { 247 + if (node.selectionSet) { 248 + inProgress.pop(); 249 + } 250 + }, 251 + }, 252 + }); 253 + 254 + let temp = output.name; 255 + // Supported cases: 256 + // - const result = await client.query() || useFragment() 257 + // - const [result] = useQuery() --> urql 258 + // - const { data } = useQuery() --> Apollo 259 + // - const { field } = useFragment() 260 + // - const [{ data }] = useQuery() 261 + // - const { data: { pokemon } } = useQuery() 262 + if ( 263 + ts.isArrayBindingPattern(temp) && 264 + ts.isBindingElement(temp.elements[0]) 265 + ) { 266 + temp = temp.elements[0].name; 267 + } 268 + 269 + let allAccess: string[] = []; 270 + if (ts.isObjectBindingPattern(temp)) { 271 + allAccess = traverseDestructuring(temp, [], allFields, source, info); 272 + } else { 273 + allAccess = crawlScope(temp, [], allFields, source, info); 274 + } 275 + 276 + const unused = allPaths.filter(x => !allAccess.includes(x)); 277 + unused.forEach(unusedField => { 278 + const loc = fieldToLoc.get(unusedField); 279 + if (!loc) return; 280 + 281 + diagnostics.push({ 282 + file: source, 283 + length: loc.length, 284 + start: node.getStart() + loc.start + 1, 285 + category: ts.DiagnosticCategory.Warning, 286 + code: UNUSED_FIELD_CODE, 287 + messageText: `Field '${unusedField}' is not used.`, 288 + }); 289 + }); 290 + }); 291 + }); 292 + 293 + return diagnostics; 294 + };