···
import { LRUCache } from 'lru-cache';
import fnv1a from '@sindresorhus/fnv1a';
findAllTaggedTemplateNodes,
import { resolveTemplate } from './ast/resolve';
import { generateTypedDocumentNodes } from './graphql/generateTypes';
25
-
import { Logger } from '.';
22
+
import { checkFieldUsageInFile } from './fieldUsage';
23
+
import { checkImportsForFragments } from './checkImports';
const clientDirectives = new Set([
···
export const SEMANTIC_DIAGNOSTIC_CODE = 52001;
export const MISSING_OPERATION_NAME_CODE = 52002;
44
-
export const MISSING_FRAGMENT_CODE = 52003;
export const USING_DEPRECATED_FIELD_CODE = 52004;
46
-
export const UNUSED_FIELD_CODE = 52005;
let isGeneratingTypes = false;
···
: checkImportsForFragments(source, info);
return [...tsDiagnostics, ...importDiagnostics];
322
-
const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => {
323
-
let node: any = start;
325
-
while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) {
326
-
node = node.parent;
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];
344
-
binding.propertyName &&
345
-
allFields.includes(binding.propertyName.getText()) &&
346
-
!originalWip.includes(binding.propertyName.getText())
348
-
wip.push(binding.propertyName.getText());
350
-
const traverseResult = traverseDestructuring(
358
-
results.push(...traverseResult);
359
-
} else if (ts.isIdentifier(binding.name)) {
360
-
const wip = [...originalWip];
362
-
binding.propertyName &&
363
-
allFields.includes(binding.propertyName.getText()) &&
364
-
!originalWip.includes(binding.propertyName.getText())
366
-
wip.push(binding.propertyName.getText());
368
-
wip.push(binding.name.getText());
371
-
const crawlResult = crawlScope(
379
-
results.push(...crawlResult);
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[] = [];
395
-
const references = info.languageService.getReferencesAtPosition(
400
-
if (!references) return results;
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
411
-
node.getStart() <= ref.textSpan.start &&
412
-
node.getEnd() >= ref.textSpan.start + ref.textSpan.length
416
-
let foundRef = findNode(source, ref.textSpan.start);
417
-
if (!foundRef) return [];
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
428
-
ts.isIdentifier(foundRef) ||
429
-
ts.isPropertyAccessExpression(foundRef) ||
430
-
ts.isElementAccessExpression(foundRef) ||
431
-
ts.isVariableDeclaration(foundRef) ||
432
-
ts.isBinaryExpression(foundRef)
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
449
-
return traverseDestructuring(
458
-
ts.isIdentifier(foundRef) &&
459
-
allFields.includes(foundRef.text) &&
460
-
!pathParts.includes(foundRef.text)
462
-
pathParts.push(foundRef.text);
464
-
ts.isPropertyAccessExpression(foundRef) &&
465
-
allFields.includes(foundRef.name.text) &&
466
-
!pathParts.includes(foundRef.name.text)
468
-
pathParts.push(foundRef.name.text);
470
-
ts.isElementAccessExpression(foundRef) &&
471
-
ts.isStringLiteral(foundRef.argumentExpression) &&
472
-
allFields.includes(foundRef.argumentExpression.text) &&
473
-
!pathParts.includes(foundRef.argumentExpression.text)
475
-
pathParts.push(foundRef.argumentExpression.text);
478
-
foundRef = foundRef.parent;
481
-
return pathParts.join('.');
487
-
const checkFieldUsageInFile = (
488
-
source: ts.SourceFile,
489
-
nodes: ts.NoSubstitutionTemplateLiteral[],
490
-
info: ts.server.PluginCreateInfo
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;
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'))
505
-
const variableDeclaration = getVariableDeclaration(node);
506
-
if (!ts.isVariableDeclaration(variableDeclaration)) return;
508
-
const references = info.languageService.getReferencesAtPosition(
510
-
variableDeclaration.name.getStart()
512
-
if (!references) return;
514
-
references.forEach(ref => {
515
-
if (ref.fileName !== source.fileName) return;
517
-
let found = findNode(source, ref.textSpan.start);
518
-
while (found && !ts.isVariableStatement(found)) {
519
-
found = found.parent;
522
-
if (!found || !ts.isVariableStatement(found)) return;
524
-
const [output] = found.declarationList.declarations;
526
-
if (output.name.getText() === variableDeclaration.name.getText()) return;
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)), {
541
-
if (!reserved.includes(node.name.value)) {
542
-
allFields.push(node.name.value);
545
-
if (!node.selectionSet && !reserved.includes(node.name.value)) {
547
-
if (inProgress.length) {
548
-
p = inProgress.join('.') + '.' + node.name.value;
550
-
p = node.name.value;
554
-
fieldToLoc.set(p, {
555
-
start: node.name.loc!.start,
556
-
length: node.name.loc!.end - node.name.loc!.start,
558
-
} else if (node.selectionSet) {
559
-
inProgress.push(node.name.value);
563
-
if (node.selectionSet) {
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()
579
-
ts.isArrayBindingPattern(temp) &&
580
-
ts.isBindingElement(temp.elements[0])
582
-
temp = temp.elements[0].name;
585
-
let allAccess: string[] = [];
586
-
if (ts.isObjectBindingPattern(temp)) {
587
-
allAccess = traverseDestructuring(temp, [], allFields, source, info);
589
-
allAccess = crawlScope(temp, [], allFields, source, info);
592
-
const unused = allPaths.filter(x => !allAccess.includes(x));
593
-
unused.forEach(unusedField => {
594
-
const loc = fieldToLoc.get(unusedField);
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.`,
609
-
return diagnostics;
612
-
const checkImportsForFragments = (
613
-
source: ts.SourceFile,
614
-
info: ts.server.PluginCreateInfo
616
-
const imports = findAllImports(source);
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;
626
-
const importedNames: string[] = [];
627
-
if (imp.importClause.name) {
628
-
importedNames.push(imp.importClause?.name.text);
632
-
imp.importClause.namedBindings &&
633
-
ts.isNamespaceImport(imp.importClause.namedBindings)
635
-
// TODO: we might need to warn here when the fragment is unused as a namespace import
638
-
imp.importClause.namedBindings &&
639
-
ts.isNamedImportBindings(imp.importClause.namedBindings)
641
-
imp.importClause.namedBindings.elements.forEach(el => {
642
-
importedNames.push(el.name.text);
646
-
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
647
-
if (!symbol) return;
649
-
const moduleExports = typeChecker?.getExportsOfModule(symbol);
650
-
if (!moduleExports) return;
652
-
const missingImports = moduleExports
654
-
if (importedNames.includes(exp.name)) {
658
-
const declarations = exp.getDeclarations();
659
-
const declaration = declarations?.find(x => {
660
-
// TODO: check whether the sourceFile.fileName resembles the module
665
-
if (!declaration) return;
667
-
const [template] = findAllTaggedTemplateNodes(declaration);
669
-
let node = template;
671
-
ts.isNoSubstitutionTemplateLiteral(node) ||
672
-
ts.isTemplateExpression(node)
674
-
if (ts.isTaggedTemplateExpression(node.parent)) {
675
-
node = node.parent;
681
-
const text = resolveTemplate(
683
-
node.getSourceFile().fileName,
687
-
const parsed = parse(text, { noLocation: true });
689
-
parsed.definitions.every(
690
-
x => x.kind === Kind.FRAGMENT_DEFINITION
693
-
return `'${exp.name}'`;
702
-
if (missingImports.length) {
703
-
tsDiagnostics.push({
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(
711
-
)} from ${imp.moduleSpecifier.getText()}.`,
717
-
return tsDiagnostics;
const runTypedDocumentNodes = (