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

catch errors in field usage (#188)

* catch errors in field usage

* formatting

* Update packages/graphqlsp/src/fieldUsage.ts

Changed files
+107 -102
.changeset
packages
graphqlsp
+5
.changeset/red-doors-run.md
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Catch errors in field-usage as we have been seeing TS fail to resolve references
+102 -100
packages/graphqlsp/src/fieldUsage.ts
···
!pathParts.includes(foundRef.text)
) {
const joined = [...pathParts, foundRef.text].join('.');
-
console.log('joined', JSON.stringify(joined, null, 2));
-
console.log('allFields', JSON.stringify(allFields, null, 2));
if (allFields.find(x => x.startsWith(joined))) {
pathParts.push(foundRef.text);
}
···
arrayMethods.has(foundRef.name.text) &&
ts.isCallExpression(foundRef.parent)
) {
-
console.log('found array method', foundRef.getText());
const isReduce = foundRef.name.text === 'reduce';
const isSomeOrEvery =
foundRef.name.text === 'every' || foundRef.name.text === 'some';
const callExpression = foundRef.parent;
const func = callExpression.arguments[0];
-
console.log('found func', func.getText());
if (ts.isFunctionExpression(func) || ts.isArrowFunction(func)) {
const param = func.parameters[isReduce ? 1 : 0];
const res = crawlScope(
···
source,
info
);
-
console.log('res scope', JSON.stringify(res, null, 2));
// TODO: do we need to support variable destructuring here like
// .map being used in const [x] = list.map()?
···
!pathParts.includes(foundRef.name.text)
) {
const joined = [...pathParts, foundRef.name.text].join('.');
-
console.log('joined', JSON.stringify(joined, null, 2));
-
console.log('allFields', JSON.stringify(allFields, null, 2));
if (allFields.find(x => x.startsWith(joined))) {
pathParts.push(foundRef.name.text);
}
···
const joined = [...pathParts, foundRef.argumentExpression.text].join(
'.'
);
-
console.log('joined', JSON.stringify(joined, null, 2));
-
console.log('allFields', JSON.stringify(allFields, null, 2));
if (allFields.find(x => x.startsWith(joined))) {
pathParts.push(foundRef.argumentExpression.text);
}
···
const shouldTrackFieldUsage = info.config.trackFieldUsage ?? true;
if (!shouldTrackFieldUsage) return diagnostics;
-
nodes.forEach(node => {
-
const nodeText = node.getText();
-
// Bailing for mutations/subscriptions as these could have small details
-
// for normalised cache interactions
-
if (nodeText.includes('mutation') || nodeText.includes('subscription'))
-
return;
+
try {
+
nodes.forEach(node => {
+
const nodeText = node.getText();
+
// Bailing for mutations/subscriptions as these could have small details
+
// for normalised cache interactions
+
if (nodeText.includes('mutation') || nodeText.includes('subscription'))
+
return;
-
const variableDeclaration = getVariableDeclaration(node);
-
if (!ts.isVariableDeclaration(variableDeclaration)) return;
+
const variableDeclaration = getVariableDeclaration(node);
+
if (!ts.isVariableDeclaration(variableDeclaration)) return;
+
+
const references = info.languageService.getReferencesAtPosition(
+
source.fileName,
+
variableDeclaration.name.getStart()
+
);
+
if (!references) return;
-
const references = info.languageService.getReferencesAtPosition(
-
source.fileName,
-
variableDeclaration.name.getStart()
-
);
-
if (!references) return;
+
const allAccess: string[] = [];
+
const inProgress: string[] = [];
+
const allPaths: 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 (!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);
-
const allAccess: string[] = [];
-
const inProgress: string[] = [];
-
const allPaths: 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 (!node.selectionSet && !reserved.includes(node.name.value)) {
-
let p;
-
if (inProgress.length) {
-
p = inProgress.join('.') + '.' + node.name.value;
-
} else {
-
p = node.name.value;
+
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);
}
-
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();
-
}
+
},
+
leave: node => {
+
if (node.selectionSet) {
+
inProgress.pop();
+
}
+
},
},
-
},
-
});
+
});
-
references.forEach(ref => {
-
if (ref.fileName !== source.fileName) return;
+
references.forEach(ref => {
+
if (ref.fileName !== source.fileName) return;
-
let found = findNode(source, ref.textSpan.start);
-
while (found && !ts.isVariableStatement(found)) {
-
found = found.parent;
-
}
+
let found = findNode(source, ref.textSpan.start);
+
while (found && !ts.isVariableStatement(found)) {
+
found = found.parent;
+
}
-
if (!found || !ts.isVariableStatement(found)) return;
+
if (!found || !ts.isVariableStatement(found)) return;
-
const [output] = found.declarationList.declarations;
+
const [output] = found.declarationList.declarations;
-
if (output.name.getText() === variableDeclaration.name.getText()) return;
+
if (output.name.getText() === variableDeclaration.name.getText())
+
return;
-
let temp = output.name;
-
// Supported cases:
-
// - const result = await client.query() || useFragment()
-
// - const [result] = useQuery() --> urql
-
// - const { data } = useQuery() --> Apollo
-
// - const { field } = useFragment()
-
// - const [{ data }] = useQuery()
-
// - const { data: { pokemon } } = useQuery()
-
if (
-
ts.isArrayBindingPattern(temp) &&
-
ts.isBindingElement(temp.elements[0])
-
) {
-
temp = temp.elements[0].name;
-
}
+
let temp = output.name;
+
// Supported cases:
+
// - const result = await client.query() || useFragment()
+
// - const [result] = useQuery() --> urql
+
// - const { data } = useQuery() --> Apollo
+
// - const { field } = useFragment()
+
// - const [{ data }] = useQuery()
+
// - const { data: { pokemon } } = useQuery()
+
if (
+
ts.isArrayBindingPattern(temp) &&
+
ts.isBindingElement(temp.elements[0])
+
) {
+
temp = temp.elements[0].name;
+
}
-
if (ts.isObjectBindingPattern(temp)) {
-
const result = traverseDestructuring(temp, [], allPaths, source, info);
-
allAccess.push(...result);
-
} else {
-
const result = crawlScope(temp, [], allPaths, source, info);
-
allAccess.push(...result);
-
}
-
});
+
if (ts.isObjectBindingPattern(temp)) {
+
const result = traverseDestructuring(
+
temp,
+
[],
+
allPaths,
+
source,
+
info
+
);
+
allAccess.push(...result);
+
} else {
+
const result = crawlScope(temp, [], allPaths, source, info);
+
allAccess.push(...result);
+
}
+
});
-
const unused = allPaths.filter(x => !allAccess.includes(x));
+
const unused = allPaths.filter(x => !allAccess.includes(x));
-
unused.forEach(unusedField => {
-
const loc = fieldToLoc.get(unusedField);
-
if (!loc) return;
+
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.`,
+
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.`,
+
});
});
});
-
});
+
} catch (e: any) {
+
console.error('[GraphQLSP]: ', e.message, e.stack);
+
}
return diagnostics;
};
-2
packages/graphqlsp/src/graphql/getSchema.ts
···
else return response.text();
})
.then(result => {
-
// TODO: Prevent logging entire result or disable logging by default
-
logger(`Got result ${JSON.stringify(result)}`);
if (typeof result === 'string') {
logger(`Got error while fetching introspection ${result}`);
} else if (result.data) {