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

feat: bubble up unused parents (#258)

Changed files
+77 -122
.changeset
packages
graphqlsp
test
+5
.changeset/thin-fishes-fry.md
···
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Bubble up unused fields to their closest parent
+1 -1
package.json
···
"prepare": "husky install",
"dev": "pnpm --filter @0no-co/graphqlsp dev",
"launch-debug": "./scripts/launch-debug.sh",
-
"test:e2e": "vitest run --single-thread"
},
"prettier": {
"singleQuote": true,
···
"prepare": "husky install",
"dev": "pnpm --filter @0no-co/graphqlsp dev",
"launch-debug": "./scripts/launch-debug.sh",
+
"test:e2e": "vitest run unused -u --single-thread"
},
"prettier": {
"singleQuote": true,
+28 -6
packages/graphqlsp/src/fieldUsage.ts
···
Field: {
enter(node) {
const alias = node.alias ? node.alias.value : node.name.value;
if (!node.selectionSet && !reservedKeys.has(node.name.value)) {
-
const path = inProgress.length
-
? `${inProgress.join('.')}.${alias}`
-
: alias;
allPaths.push(path);
fieldToLoc.set(path, {
start: node.name.loc!.start,
···
});
} else if (node.selectionSet) {
inProgress.push(alias);
}
},
leave(node) {
···
}
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.`,
});
});
});
···
Field: {
enter(node) {
const alias = node.alias ? node.alias.value : node.name.value;
+
const path = inProgress.length
+
? `${inProgress.join('.')}.${alias}`
+
: alias;
+
if (!node.selectionSet && !reservedKeys.has(node.name.value)) {
allPaths.push(path);
fieldToLoc.set(path, {
start: node.name.loc!.start,
···
});
} else if (node.selectionSet) {
inProgress.push(alias);
+
fieldToLoc.set(path, {
+
start: node.name.loc!.start,
+
length: node.name.loc!.end - node.name.loc!.start,
+
});
}
},
leave(node) {
···
}
const unused = allPaths.filter(x => !allAccess.includes(x));
+
const aggregatedUnusedFields = new Set<string>();
+
const unusedChildren: { [key: string]: Set<string> } = {};
unused.forEach(unusedField => {
+
const split = unusedField.split('.');
+
split.pop();
+
const parentField = split.join('.');
+
const loc = fieldToLoc.get(parentField);
if (!loc) return;
+
aggregatedUnusedFields.add(parentField);
+
if (unusedChildren[parentField]) {
+
unusedChildren[parentField].add(unusedField);
+
} else {
+
unusedChildren[parentField] = new Set([unusedField]);
+
}
+
});
+
+
aggregatedUnusedFields.forEach(field => {
+
const loc = fieldToLoc.get(field)!;
+
const unusedFields = unusedChildren[field]!;
diagnostics.push({
file: source,
length: loc.length,
start: node.getStart() + loc.start + 1,
category: ts.DiagnosticCategory.Warning,
code: UNUSED_FIELD_CODE,
+
messageText: `Field(s) ${[...unusedFields]
+
.map(x => `'${x}'`)
+
.join(', ')} are not used.`,
});
});
});
+43 -108
test/e2e/unused-fieds.test.ts
···
"category": "warning",
"code": 52005,
"end": {
-
"line": 10,
-
"offset": 15,
},
"start": {
-
"line": 10,
-
"offset": 9,
-
},
-
"text": "Field 'attacks.fast.damage' is not used.",
-
},
-
{
-
"category": "warning",
-
"code": 52005,
-
"end": {
-
"line": 11,
-
"offset": 13,
-
},
-
"start": {
-
"line": 11,
-
"offset": 9,
},
-
"text": "Field 'attacks.fast.name' is not used.",
},
]
`);
···
"category": "warning",
"code": 52005,
"end": {
-
"line": 10,
-
"offset": 15,
-
},
-
"start": {
-
"line": 10,
-
"offset": 9,
-
},
-
"text": "Field 'attacks.fast.damage' is not used.",
-
},
-
{
-
"category": "warning",
-
"code": 52005,
-
"end": {
-
"line": 11,
-
"offset": 13,
},
"start": {
-
"line": 11,
-
"offset": 9,
},
-
"text": "Field 'attacks.fast.name' is not used.",
},
]
`);
···
"category": "warning",
"code": 52005,
"end": {
-
"line": 11,
-
"offset": 15,
-
},
-
"start": {
-
"line": 11,
-
"offset": 7,
-
},
-
"text": "Field 'pokemon.fleeRate' is not used.",
-
},
-
{
-
"category": "warning",
-
"code": 52005,
-
"end": {
-
"line": 16,
-
"offset": 17,
},
"start": {
-
"line": 16,
-
"offset": 11,
},
-
"text": "Field 'pokemon.attacks.special.damage' is not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
-
"line": 20,
"offset": 16,
},
"start": {
-
"line": 20,
"offset": 9,
},
-
"text": "Field 'pokemon.weight.minimum' is not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
-
"line": 21,
-
"offset": 16,
},
"start": {
-
"line": 21,
-
"offset": 9,
},
-
"text": "Field 'pokemon.weight.maximum' is not used.",
},
{
"category": "error",
···
"category": "warning",
"code": 52005,
"end": {
-
"line": 15,
-
"offset": 15,
},
"start": {
-
"line": 15,
-
"offset": 11,
},
-
"text": "Field 'pokemon.attacks.special.name' is not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
-
"line": 16,
-
"offset": 17,
},
"start": {
-
"line": 16,
-
"offset": 11,
},
-
"text": "Field 'pokemon.attacks.special.damage' is not used.",
-
},
-
{
-
"category": "warning",
-
"code": 52005,
-
"end": {
-
"line": 23,
-
"offset": 11,
-
},
-
"start": {
-
"line": 23,
-
"offset": 7,
-
},
-
"text": "Field 'pokemon.name' is not used.",
},
{
"category": "error",
···
"category": "warning",
"code": 52005,
"end": {
-
"line": 15,
-
"offset": 15,
},
"start": {
-
"line": 15,
-
"offset": 11,
},
-
"text": "Field 'pokemon.attacks.special.name' is not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
-
"line": 16,
-
"offset": 17,
},
"start": {
-
"line": 16,
-
"offset": 11,
-
},
-
"text": "Field 'pokemon.attacks.special.damage' is not used.",
-
},
-
{
-
"category": "warning",
-
"code": 52005,
-
"end": {
-
"line": 23,
-
"offset": 11,
},
-
"start": {
-
"line": 23,
-
"offset": 7,
-
},
-
"text": "Field 'pokemon.name' is not used.",
},
{
"category": "error",
···
"category": "warning",
"code": 52005,
"end": {
+
"line": 9,
+
"offset": 11,
},
"start": {
+
"line": 9,
+
"offset": 7,
},
+
"text": "Field(s) 'attacks.fast.damage', 'attacks.fast.name' are not used.",
},
]
`);
···
"category": "warning",
"code": 52005,
"end": {
+
"line": 9,
+
"offset": 11,
},
"start": {
+
"line": 9,
+
"offset": 7,
},
+
"text": "Field(s) 'attacks.fast.damage', 'attacks.fast.name' are not used.",
},
]
`);
···
"category": "warning",
"code": 52005,
"end": {
+
"line": 9,
+
"offset": 12,
},
"start": {
+
"line": 9,
+
"offset": 5,
},
+
"text": "Field(s) 'pokemon.fleeRate' are not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
+
"line": 14,
"offset": 16,
},
"start": {
+
"line": 14,
"offset": 9,
},
+
"text": "Field(s) 'pokemon.attacks.special.damage' are not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
+
"line": 19,
+
"offset": 13,
},
"start": {
+
"line": 19,
+
"offset": 7,
},
+
"text": "Field(s) 'pokemon.weight.minimum', 'pokemon.weight.maximum' are not used.",
},
{
"category": "error",
···
"category": "warning",
"code": 52005,
"end": {
+
"line": 14,
+
"offset": 16,
},
"start": {
+
"line": 14,
+
"offset": 9,
},
+
"text": "Field(s) 'pokemon.attacks.special.name', 'pokemon.attacks.special.damage' are not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
+
"line": 9,
+
"offset": 12,
},
"start": {
+
"line": 9,
+
"offset": 5,
},
+
"text": "Field(s) 'pokemon.name' are not used.",
},
{
"category": "error",
···
"category": "warning",
"code": 52005,
"end": {
+
"line": 14,
+
"offset": 16,
},
"start": {
+
"line": 14,
+
"offset": 9,
},
+
"text": "Field(s) 'pokemon.attacks.special.name', 'pokemon.attacks.special.damage' are not used.",
},
{
"category": "warning",
"code": 52005,
"end": {
+
"line": 9,
+
"offset": 12,
},
"start": {
+
"line": 9,
+
"offset": 5,
},
+
"text": "Field(s) 'pokemon.name' are not used.",
},
{
"category": "error",
-7
tsconfig.json
···
{
"compilerOptions": {
-
"plugins": [
-
{
-
"name": "@0no-co/graphqlsp",
-
"schema": "./packages/example-tada/schema.graphql",
-
"tadaOutputLocation": "./packages/example-tada/introspection.ts"
-
}
-
],
"jsx": "react-jsx",
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
···
{
"compilerOptions": {
"jsx": "react-jsx",
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,