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

fix(field usage): avoid checking usage multiple times (#170)

* fix field usage in tada

* remove logger

* test update

* usage tracking

Changed files
+67 -59
.changeset
packages
example-tada
graphqlsp
test
e2e
fixture-project-client-preset
+5
.changeset/new-feet-travel.md
···
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Ensure we track usage across all references
+1 -1
packages/example-tada/introspection.ts
···
* @example
* ```
* import { initGraphQLTada } from 'gql.tada';
-
* import { introspection } from './introspection';
*
* export const graphql = initGraphQLTada<{
* introspection: typeof introspection;
···
* @example
* ```
* import { initGraphQLTada } from 'gql.tada';
+
* import type { introspection } from './introspection';
*
* export const graphql = initGraphQLTada<{
* introspection: typeof introspection;
+60 -57
packages/graphqlsp/src/fieldUsage.ts
···
);
if (!references) return;
references.forEach(ref => {
if (ref.fileName !== source.fileName) return;
···
if (output.name.getText() === variableDeclaration.name.getText()) return;
-
const inProgress: string[] = [];
-
const allPaths: string[] = [];
-
const allFields: string[] = [];
-
const reserved = ['id', '__typename'];
-
const fieldToLoc = new Map<string, { start: number; length: number }>();
-
// This visitor gets all the leaf-paths in the document
-
// as well as all fields that are part of the document
-
// We need the leaf-paths to check usage and we need the
-
// fields to validate whether an access on a given reference
-
// is valid given the current document...
-
visit(parse(node.getText().slice(1, -1)), {
-
Field: {
-
enter: node => {
-
if (!reserved.includes(node.name.value)) {
-
allFields.push(node.name.value);
-
}
-
-
if (!node.selectionSet && !reserved.includes(node.name.value)) {
-
let p;
-
if (inProgress.length) {
-
p = inProgress.join('.') + '.' + node.name.value;
-
} else {
-
p = node.name.value;
-
}
-
allPaths.push(p);
-
-
fieldToLoc.set(p, {
-
start: node.name.loc!.start,
-
length: node.name.loc!.end - node.name.loc!.start,
-
});
-
} else if (node.selectionSet) {
-
inProgress.push(node.name.value);
-
}
-
},
-
leave: node => {
-
if (node.selectionSet) {
-
inProgress.pop();
-
}
-
},
-
},
-
});
-
let temp = output.name;
// Supported cases:
// - const result = await client.query() || useFragment()
···
temp = temp.elements[0].name;
}
-
let allAccess: string[] = [];
if (ts.isObjectBindingPattern(temp)) {
-
allAccess = traverseDestructuring(temp, [], allFields, source, info);
} else {
-
allAccess = crawlScope(temp, [], allFields, source, info);
}
-
const unused = allPaths.filter(x => !allAccess.includes(x));
-
unused.forEach(unusedField => {
-
const loc = fieldToLoc.get(unusedField);
-
if (!loc) return;
-
diagnostics.push({
-
file: source,
-
length: loc.length,
-
start: node.getStart() + loc.start + 1,
-
category: ts.DiagnosticCategory.Warning,
-
code: UNUSED_FIELD_CODE,
-
messageText: `Field '${unusedField}' is not used.`,
-
});
});
});
});
···
);
if (!references) return;
+
const allAccess: string[] = [];
+
const inProgress: string[] = [];
+
const allPaths: string[] = [];
+
const allFields: string[] = [];
+
const reserved = ['id', '__typename'];
+
const fieldToLoc = new Map<string, { start: number; length: number }>();
+
// This visitor gets all the leaf-paths in the document
+
// as well as all fields that are part of the document
+
// We need the leaf-paths to check usage and we need the
+
// fields to validate whether an access on a given reference
+
// is valid given the current document...
+
visit(parse(node.getText().slice(1, -1)), {
+
Field: {
+
enter: node => {
+
if (!reserved.includes(node.name.value)) {
+
allFields.push(node.name.value);
+
}
+
+
if (!node.selectionSet && !reserved.includes(node.name.value)) {
+
let p;
+
if (inProgress.length) {
+
p = inProgress.join('.') + '.' + node.name.value;
+
} else {
+
p = node.name.value;
+
}
+
allPaths.push(p);
+
+
fieldToLoc.set(p, {
+
start: node.name.loc!.start,
+
length: node.name.loc!.end - node.name.loc!.start,
+
});
+
} else if (node.selectionSet) {
+
inProgress.push(node.name.value);
+
}
+
},
+
leave: node => {
+
if (node.selectionSet) {
+
inProgress.pop();
+
}
+
},
+
},
+
});
+
references.forEach(ref => {
if (ref.fileName !== source.fileName) return;
···
if (output.name.getText() === variableDeclaration.name.getText()) return;
let temp = output.name;
// Supported cases:
// - const result = await client.query() || useFragment()
···
temp = temp.elements[0].name;
}
if (ts.isObjectBindingPattern(temp)) {
+
const result = traverseDestructuring(temp, [], allFields, source, info);
+
allAccess.push(...result);
} else {
+
const result = crawlScope(temp, [], allFields, source, info);
+
allAccess.push(...result);
}
+
});
+
const unused = allPaths.filter(x => !allAccess.includes(x));
+
+
unused.forEach(unusedField => {
+
const loc = fieldToLoc.get(unusedField);
+
if (!loc) return;
+
diagnostics.push({
+
file: source,
+
length: loc.length,
+
start: node.getStart() + loc.start + 1,
+
category: ts.DiagnosticCategory.Warning,
+
code: UNUSED_FIELD_CODE,
+
messageText: `Field '${unusedField}' is not used.`,
});
});
});
+1 -1
test/e2e/fixture-project-client-preset/tsconfig.json
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
-
"shouldCheckForColocatedFragments": true
}
],
"target": "es2016",
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
+
"trackFieldUsage": false
}
],
"target": "es2016",