Skip to content

Commit

Permalink
Crawl nested scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Sep 28, 2024
1 parent acede9b commit 9279662
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 55 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-queens-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphqlsp': patch
---

Handle chained expressions while crawling scopes
159 changes: 104 additions & 55 deletions packages/graphqlsp/src/fieldUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ts } from './ts';
import { parse, visit } from 'graphql';

import { findNode } from './ast';
import { PropertyAccessExpression } from 'typescript';

export const UNUSED_FIELD_CODE = 52005;

Expand Down Expand Up @@ -119,6 +120,82 @@ const arrayMethods = new Set([
'sort',
]);

const crawlChainedExpressions = (
ref: ts.CallExpression,
pathParts: string[],
allFields: string[],
source: ts.SourceFile,
info: ts.server.PluginCreateInfo
): string[] => {
const isChained =
ts.isPropertyAccessExpression(ref.expression) &&
arrayMethods.has(ref.expression.name.text);
console.log('[GRAPHQLSP]: ', isChained, ref.getFullText());
if (isChained) {
const foundRef = ref.expression;
const isReduce = foundRef.name.text === 'reduce';
let func: ts.Expression | ts.FunctionDeclaration | undefined =
ref.arguments[0];

const res = [];
if (ts.isCallExpression(ref.parent.parent)) {
const nestedResult = crawlChainedExpressions(
ref.parent.parent,
pathParts,
allFields,
source,
info
);
if (nestedResult.length) {
res.push(...nestedResult);
}
}

if (func && ts.isIdentifier(func)) {
// TODO: Scope utilities in checkFieldUsageInFile to deduplicate
const checker = info.languageService.getProgram()!.getTypeChecker();

const declaration = checker.getSymbolAtLocation(func)?.valueDeclaration;
if (declaration && ts.isFunctionDeclaration(declaration)) {
func = declaration;
} else if (
declaration &&
ts.isVariableDeclaration(declaration) &&
declaration.initializer
) {
func = declaration.initializer;
}
}

if (
func &&
(ts.isFunctionDeclaration(func) ||
ts.isFunctionExpression(func) ||
ts.isArrowFunction(func))
) {
const param = func.parameters[isReduce ? 1 : 0];
if (param) {
const scopedResult = crawlScope(
param.name,
pathParts,
allFields,
source,
info,
true
);

if (scopedResult.length) {
res.push(...scopedResult);
}
}
}

return res;
}

return [];
};

const crawlScope = (
node: ts.BindingName,
originalWip: Array<string>,
Expand Down Expand Up @@ -173,6 +250,7 @@ const crawlScope = (
// - const pokemon = result.data.pokemon --> this initiates a new crawl with a renewed scope
// - const { pokemon } = result.data --> this initiates a destructuring traversal which will
// either end up in more destructuring traversals or a scope crawl
console.log('[GRAPHQLSP]: ', foundRef.getFullText());
while (
ts.isIdentifier(foundRef) ||
ts.isPropertyAccessExpression(foundRef) ||
Expand Down Expand Up @@ -219,65 +297,36 @@ const crawlScope = (
arrayMethods.has(foundRef.name.text) &&
ts.isCallExpression(foundRef.parent)
) {
const isReduce = foundRef.name.text === 'reduce';
const isSomeOrEvery =
foundRef.name.text === 'every' || foundRef.name.text === 'some';
const callExpression = foundRef.parent;
let func: ts.Expression | ts.FunctionDeclaration | undefined =
callExpression.arguments[0];

if (func && ts.isIdentifier(func)) {
// TODO: Scope utilities in checkFieldUsageInFile to deduplicate
const checker = info.languageService.getProgram()!.getTypeChecker();

const declaration =
checker.getSymbolAtLocation(func)?.valueDeclaration;
if (declaration && ts.isFunctionDeclaration(declaration)) {
func = declaration;
} else if (
declaration &&
ts.isVariableDeclaration(declaration) &&
declaration.initializer
) {
func = declaration.initializer;
}
const res = [];
const isSomeOrEvery =
foundRef.name.text === 'some' || foundRef.name.text === 'every';
console.log('[GRAPHQLSP]: ', foundRef.name.text);
const chainedResults = crawlChainedExpressions(
callExpression,
pathParts,
allFields,
source,
info
);
console.log('[GRAPHQLSP]: ', chainedResults.length);
if (chainedResults.length) {
res.push(...chainedResults);
}

if (
func &&
(ts.isFunctionDeclaration(func) ||
ts.isFunctionExpression(func) ||
ts.isArrowFunction(func))
) {
const param = func.parameters[isReduce ? 1 : 0];
if (param) {
const res = crawlScope(
param.name,
pathParts,
allFields,
source,
info,
true
);

if (
ts.isVariableDeclaration(callExpression.parent) &&
!isSomeOrEvery
) {
const varRes = crawlScope(
callExpression.parent.name,
pathParts,
allFields,
source,
info,
true
);
res.push(...varRes);
}

return res;
}
if (ts.isVariableDeclaration(callExpression.parent) && !isSomeOrEvery) {
const varRes = crawlScope(
callExpression.parent.name,
pathParts,
allFields,
source,
info,
true
);
res.push(...varRes);
}

return res;
} else if (
ts.isPropertyAccessExpression(foundRef) &&
!pathParts.includes(foundRef.name.text)
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/fixture-project-tada/introspection.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ export type introspection = {
};

import * as gqlTada from 'gql.tada';

declare module 'gql.tada' {
interface setupSchema {
introspection: introspection;
}
}
37 changes: 37 additions & 0 deletions test/e2e/fixture-project-unused-fields/fixtures/chained-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useQuery } from 'urql';
import { useMemo } from 'react';
import { graphql } from './gql';

const PokemonsQuery = graphql(
`
query Pok {
pokemons {
name
maxCP
maxHP
fleeRate
}
}
`
);

const Pokemons = () => {
const [result] = useQuery({
query: PokemonsQuery,
});

const results = useMemo(() => {
if (!result.data?.pokemons) return [];
return (
result.data.pokemons
.filter(i => i?.name === 'Pikachu')
.map(p => ({
x: p?.maxCP,
y: p?.maxHP,
})) ?? []
);
}, [result.data?.pokemons]);

// @ts-ignore
return results;
};
41 changes: 41 additions & 0 deletions test/e2e/fixture-project-unused-fields/fixtures/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ export type PoQuery = {
| null;
};

export type PokQueryVariables = Exact<{ [key: string]: never }>;

export type PokQuery = {
__typename?: 'Query';
pokemons?: Array<{
__typename?: 'Pokemon';
name: string;
maxCP?: number | null;
maxHP?: number | null;
fleeRate?: number | null;
} | null> | null;
};

export const PokemonFieldsFragmentDoc = {
kind: 'Document',
definitions: [
Expand Down Expand Up @@ -338,3 +351,31 @@ export const PoDocument = {
},
],
} as unknown as DocumentNode<PoQuery, PoQueryVariables>;
export const PokDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'Pok' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'pokemons' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'maxCP' } },
{ kind: 'Field', name: { kind: 'Name', value: 'maxHP' } },
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<PokQuery, PokQueryVariables>;
44 changes: 44 additions & 0 deletions test/e2e/unused-fieds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('unused fields', () => {
);
const outfileFragment = path.join(projectPath, 'fragment.tsx');
const outfilePropAccess = path.join(projectPath, 'property-access.tsx');
const outfileChainedUsage = path.join(projectPath, 'chained-usage.ts');

let server: TSServer;
beforeAll(async () => {
Expand Down Expand Up @@ -56,6 +57,11 @@ describe('unused fields', () => {
fileContent: '// empty',
scriptKindName: 'TS',
} satisfies ts.server.protocol.OpenRequestArgs);
server.sendCommand('open', {
file: outfileChainedUsage,
fileContent: '// empty',
scriptKindName: 'TS',
} satisfies ts.server.protocol.OpenRequestArgs);

server.sendCommand('updateOpen', {
openFiles: [
Expand Down Expand Up @@ -101,6 +107,13 @@ describe('unused fields', () => {
'utf-8'
),
},
{
file: outfileChainedUsage,
fileContent: fs.readFileSync(
path.join(projectPath, 'fixtures/chained-usage.ts'),
'utf-8'
),
},
],
} satisfies ts.server.protocol.UpdateOpenRequestArgs);

Expand Down Expand Up @@ -128,6 +141,10 @@ describe('unused fields', () => {
file: outfileBail,
tmpfile: outfileBail,
} satisfies ts.server.protocol.SavetoRequestArgs);
server.sendCommand('saveto', {
file: outfileChainedUsage,
tmpfile: outfileChainedUsage,
} satisfies ts.server.protocol.SavetoRequestArgs);
});

afterAll(() => {
Expand All @@ -138,6 +155,7 @@ describe('unused fields', () => {
fs.unlinkSync(outfileFragmentDestructuring);
fs.unlinkSync(outfileDestructuringFromStart);
fs.unlinkSync(outfileBail);
fs.unlinkSync(outfileChainedUsage);
} catch {}
});

Expand Down Expand Up @@ -405,4 +423,30 @@ describe('unused fields', () => {
]
`);
}, 30000);

it('Finds field usage in chained call-expressions', async () => {
const res = server.responses.filter(
resp =>
resp.type === 'event' &&
resp.event === 'semanticDiag' &&
resp.body?.file === outfileChainedUsage
);
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.",
},
]
`);
}, 30000);
});

0 comments on commit 9279662

Please sign in to comment.