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

feat: add bail when we return a property from a function (#260)

Changed files
+125 -10
.changeset
packages
example-tada
graphqlsp
test
e2e
fixture-project-unused-fields
fixtures
+5
.changeset/loud-dryers-burn.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Add a bail for `fieldUsage` where we return a property from a function
+4
packages/example-tada/introspection.ts
···
{
"kind": "SCALAR",
"name": "Boolean"
+
},
+
{
+
"kind": "SCALAR",
+
"name": "Any"
}
],
"directives": []
+32 -10
packages/graphqlsp/src/fieldUsage.ts
···
const wip = [...originalWip];
return ts.isIdentifier(element.name)
-
? crawlScope(element.name, wip, allFields, source, info)
+
? crawlScope(element.name, wip, allFields, source, info, false)
: ts.isObjectBindingPattern(element.name)
? traverseDestructuring(element.name, wip, allFields, source, info)
: traverseArrayDestructuring(element.name, wip, allFields, source, info);
···
wip,
allFields,
source,
-
info
+
info,
+
false
);
results.push(...crawlResult);
···
originalWip: Array<string>,
allFields: Array<string>,
source: ts.SourceFile,
-
info: ts.server.PluginCreateInfo
+
info: ts.server.PluginCreateInfo,
+
inArrayMethod: boolean
): Array<string> => {
if (ts.isObjectBindingPattern(node)) {
return traverseDestructuring(node, originalWip, allFields, source, info);
···
ts.isPropertyAccessExpression(foundRef) ||
ts.isElementAccessExpression(foundRef) ||
ts.isVariableDeclaration(foundRef) ||
-
ts.isBinaryExpression(foundRef)
+
ts.isBinaryExpression(foundRef) ||
+
ts.isReturnStatement(foundRef) ||
+
ts.isArrowFunction(foundRef)
) {
-
if (ts.isVariableDeclaration(foundRef)) {
-
return crawlScope(foundRef.name, pathParts, allFields, source, info);
+
if (
+
!inArrayMethod &&
+
(ts.isReturnStatement(foundRef) || ts.isArrowFunction(foundRef))
+
) {
+
// When we are returning the ref or we are dealing with an implicit return
+
// we mark all its children as used (bail scenario)
+
const joined = pathParts.join('.');
+
const bailedFields = allFields.filter(x => x.startsWith(joined + '.'));
+
return bailedFields;
+
} else if (ts.isVariableDeclaration(foundRef)) {
+
return crawlScope(
+
foundRef.name,
+
pathParts,
+
allFields,
+
source,
+
info,
+
false
+
);
} else if (
ts.isIdentifier(foundRef) &&
!pathParts.includes(foundRef.text)
) {
const joined = [...pathParts, foundRef.text].join('.');
-
if (allFields.find(x => x.startsWith(joined))) {
+
if (allFields.find(x => x.startsWith(joined + '.'))) {
pathParts.push(foundRef.text);
}
} else if (
···
pathParts,
allFields,
source,
-
info
+
info,
+
true
);
if (
···
pathParts,
allFields,
source,
-
info
+
info,
+
true
);
res.push(...varRes);
}
···
}
if (name) {
-
const result = crawlScope(name, [], allPaths, source, info);
+
const result = crawlScope(name, [], allPaths, source, info, false);
allAccess.push(...result);
}
});
+40
test/e2e/fixture-project-unused-fields/fixtures/bail.tsx
···
+
import * as React from 'react';
+
import { useQuery } from 'urql';
+
import { graphql } from './gql';
+
// @ts-expect-error
+
import { Pokemon } from './fragment';
+
+
const PokemonQuery = graphql(`
+
query Po($id: ID!) {
+
pokemon(id: $id) {
+
id
+
fleeRate
+
...pokemonFields
+
attacks {
+
special {
+
name
+
damage
+
}
+
}
+
weight {
+
minimum
+
maximum
+
}
+
name
+
__typename
+
}
+
}
+
`);
+
+
const Pokemons = () => {
+
const [result] = useQuery({
+
query: PokemonQuery,
+
variables: { id: '' }
+
});
+
+
const pokemon = React.useMemo(() => result.data?.pokemon, [])
+
+
// @ts-expect-error
+
return <Pokemon data={result.data?.pokemon} />;
+
}
+
+44
test/e2e/unused-fieds.test.ts
···
'immediate-destructuring.tsx'
);
const outfileDestructuring = path.join(projectPath, 'destructuring.tsx');
+
const outfileBail = path.join(projectPath, 'bail.tsx');
const outfileFragmentDestructuring = path.join(
projectPath,
'fragment-destructuring.tsx'
···
server.sendCommand('open', {
file: outfileDestructuring,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
server.sendCommand('open', {
+
file: outfileBail,
fileContent: '// empty',
scriptKindName: 'TS',
} satisfies ts.server.protocol.OpenRequestArgs);
···
),
},
{
+
file: outfileBail,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/bail.tsx'),
+
'utf-8'
+
),
+
},
+
{
file: outfileFragment,
fileContent: fs.readFileSync(
path.join(projectPath, 'fixtures/fragment.tsx'),
···
file: outfileDestructuringFromStart,
tmpfile: outfileDestructuringFromStart,
} satisfies ts.server.protocol.SavetoRequestArgs);
+
server.sendCommand('saveto', {
+
file: outfileBail,
+
tmpfile: outfileBail,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
});
afterAll(() => {
···
fs.unlinkSync(outfilePropAccess);
fs.unlinkSync(outfileFragmentDestructuring);
fs.unlinkSync(outfileDestructuringFromStart);
+
fs.unlinkSync(outfileBail);
} catch {}
});
···
},
"start": {
"line": 3,
+
"offset": 1,
+
},
+
"text": "Unused '@ts-expect-error' directive.",
+
},
+
]
+
`);
+
}, 30000);
+
+
it('Bails unused fields when memo func is used', async () => {
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfileBail
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"category": "error",
+
"code": 2578,
+
"end": {
+
"line": 4,
+
"offset": 20,
+
},
+
"start": {
+
"line": 4,
"offset": 1,
},
"text": "Unused '@ts-expect-error' directive.",