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

feat: track unused fields (#146)

* Track field usage and warn for unused fields

* cleanup crew

* comments

* readme

* fixies

* add tests

* add callout

* Use `node.getStart()` instead of `node.pos` as input to `getReferencesAtPosition` (#149)

* convert other one to getStart

* support all cases

* add tests

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

+5
.changeset/three-weeks-taste.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Track field usage and warn when a field goes unused
+25
README.md
···
- `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
unused fragments and provide a message notifying you about them
+
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
+
unused fields within the same file.
### GraphQL Code Generator client-preset
···
"schema": "./schema.graphql",
"disableTypegen": true,
"templateIsCallExpression": true,
+
"trackFieldUsage": true,
"template": "graphql"
}
]
}
}
```
+
+
## Tracking unused fields
+
+
Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost
+
it will only track in the same file to encourage [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments).
+
Secondly it supports a few patterns which we'll add to as time progresses:
+
+
```ts
+
// Supported cases:
+
const result = (await client.query()) || useFragment();
+
const [result] = useQuery(); // --> urql
+
const { data } = useQuery(); // --> Apollo
+
// Missing cases:
+
const { field } = useFragment(); // can't destructure into your data from the start
+
const [{ data }] = useQuery(); // can't follow array destructuring with object destructuring
+
const {
+
data: { pokemon },
+
} = useQuery(); // can't destructure into your data from the start
+
```
+
+
Lastly we don't track mutations/subscriptions as some folks will add additional fields to properly support
+
normalised cache updates.
## Fragment masking
+3 -1
packages/example-external-generator/package.json
···
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"@urql/core": "^3.0.0",
-
"graphql": "^16.8.1"
+
"graphql": "^16.8.1",
+
"urql": "^4.0.6"
},
"devDependencies": {
"@0no-co/graphqlsp": "file:../graphqlsp",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/client-preset": "^4.1.0",
+
"@types/react": "^18.2.45",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
}
-6
packages/example-external-generator/src/Pokemon.tsx
···
}
`)
-
export const WeakFields = graphql(`
-
fragment weaknessFields on Pokemon {
-
weaknesses
-
}
-
`)
-
export const Pokemon = (data: any) => {
const pokemon = useFragment(PokemonFields, data);
return `hi ${pokemon.name}`;
+3 -27
packages/example-external-generator/src/gql/gql.ts
···
const documents = {
'\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n':
types.PokemonFieldsFragmentDoc,
-
'\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n':
-
types.WeaknessFieldsFragmentDoc,
-
'\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n':
-
types.PokDocument,
-
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n':
+
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n':
types.PoDocument,
-
'\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n':
-
types.PokemonsAreAwesomeDocument,
};
/**
···
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
-
source: '\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n'
-
): (typeof documents)['\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n'];
-
/**
-
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
-
*/
-
export function graphql(
-
source: '\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n'
-
): (typeof documents)['\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n'];
-
/**
-
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
-
*/
-
export function graphql(
-
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n'
-
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n'];
-
/**
-
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
-
*/
-
export function graphql(
-
source: '\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n'
-
): (typeof documents)['\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n'];
+
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'
+
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
+69 -162
packages/example-external-generator/src/gql/graphql.ts
···
} | null;
} & { ' $fragmentName'?: 'PokemonFieldsFragment' };
-
export type WeaknessFieldsFragment = {
-
__typename?: 'Pokemon';
-
weaknesses?: Array<PokemonType | null> | null;
-
} & { ' $fragmentName'?: 'WeaknessFieldsFragment' };
-
-
export type PokQueryVariables = Exact<{
-
limit: Scalars['Int']['input'];
+
export type PoQueryVariables = Exact<{
+
id: Scalars['ID']['input'];
}>;
-
export type PokQuery = {
+
export type PoQuery = {
__typename?: 'Query';
-
pokemons?: Array<
+
pokemon?:
| ({
__typename: 'Pokemon';
id: string;
+
fleeRate?: number | null;
name: string;
-
fleeRate?: number | null;
-
classification?: string | null;
+
attacks?: {
+
__typename?: 'AttacksConnection';
+
special?: Array<{
+
__typename?: 'Attack';
+
name?: string | null;
+
damage?: number | null;
+
} | null> | null;
+
} | null;
+
weight?: {
+
__typename?: 'PokemonDimension';
+
minimum?: string | null;
+
maximum?: string | null;
+
} | null;
} & {
-
' $fragmentRefs'?: {
-
PokemonFieldsFragment: PokemonFieldsFragment;
-
WeaknessFieldsFragment: WeaknessFieldsFragment;
-
};
+
' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment };
})
-
| null
-
> | null;
-
};
-
-
export type PoQueryVariables = Exact<{
-
id: Scalars['ID']['input'];
-
}>;
-
-
export type PoQuery = {
-
__typename?: 'Query';
-
pokemon?: {
-
__typename: 'Pokemon';
-
id: string;
-
fleeRate?: number | null;
-
} | null;
-
};
-
-
export type PokemonsAreAwesomeQueryVariables = Exact<{ [key: string]: never }>;
-
-
export type PokemonsAreAwesomeQuery = {
-
__typename?: 'Query';
-
pokemons?: Array<{ __typename?: 'Pokemon'; id: string } | null> | null;
+
| null;
};
export const PokemonFieldsFragmentDoc = {
···
},
],
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
-
export const WeaknessFieldsFragmentDoc = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'weaknessFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<WeaknessFieldsFragment, unknown>;
-
export const PokDocument = {
+
export const PoDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
-
name: { kind: 'Name', value: 'Pok' },
+
name: { kind: 'Name', value: 'Po' },
variableDefinitions: [
{
kind: 'VariableDefinition',
-
variable: {
-
kind: 'Variable',
-
name: { kind: 'Name', value: 'limit' },
-
},
+
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
type: {
kind: 'NonNullType',
-
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } },
+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
···
selections: [
{
kind: 'Field',
-
name: { kind: 'Name', value: 'pokemons' },
+
name: { kind: 'Name', value: 'pokemon' },
arguments: [
{
kind: 'Argument',
-
name: { kind: 'Name', value: 'limit' },
+
name: { kind: 'Name', value: 'id' },
value: {
kind: 'Variable',
-
name: { kind: 'Name', value: 'limit' },
+
name: { kind: 'Name', value: 'id' },
},
},
],
···
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'classification' },
+
kind: 'FragmentSpread',
+
name: { kind: 'Name', value: 'pokemonFields' },
},
{
-
kind: 'FragmentSpread',
-
name: { kind: 'Name', value: 'pokemonFields' },
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'special' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'name' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
],
+
},
+
},
+
],
+
},
},
{
-
kind: 'FragmentSpread',
-
name: { kind: 'Name', value: 'weaknessFields' },
+
kind: 'Field',
+
name: { kind: 'Name', value: 'weight' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'minimum' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'maximum' },
+
},
+
],
+
},
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
],
},
···
],
},
},
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'weaknessFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<PokQuery, PokQueryVariables>;
-
export const PoDocument = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'OperationDefinition',
-
operation: 'query',
-
name: { kind: 'Name', value: 'Po' },
-
variableDefinitions: [
-
{
-
kind: 'VariableDefinition',
-
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
-
type: {
-
kind: 'NonNullType',
-
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
-
},
-
},
-
],
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'pokemon' },
-
arguments: [
-
{
-
kind: 'Argument',
-
name: { kind: 'Name', value: 'id' },
-
value: {
-
kind: 'Variable',
-
name: { kind: 'Name', value: 'id' },
-
},
-
},
-
],
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
-
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
-
],
-
},
-
},
-
],
-
},
-
},
],
} as unknown as DocumentNode<PoQuery, PoQueryVariables>;
-
export const PokemonsAreAwesomeDocument = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'OperationDefinition',
-
operation: 'query',
-
name: { kind: 'Name', value: 'PokemonsAreAwesome' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'pokemons' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
],
-
},
-
},
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<
-
PokemonsAreAwesomeQuery,
-
PokemonsAreAwesomeQueryVariables
-
>;
+37 -31
packages/example-external-generator/src/index.tsx
···
-
import { createClient } from '@urql/core';
+
import { createClient, useQuery } from 'urql';
import { graphql } from './gql';
-
-
const x = graphql(`
-
query Pok($limit: Int!) {
-
pokemons(limit: $limit) @populate {
-
id
-
name
-
fleeRate
-
classification
-
...pokemonFields
-
...weaknessFields
-
__typename
-
}
-
}
-
`)
-
-
const client = createClient({
-
url: '',
-
});
+
import { Pokemon } from './Pokemon';
const PokemonQuery = graphql(`
query Po($id: ID!) {
pokemon(id: $id) {
id
fleeRate
+
...pokemonFields
+
attacks {
+
special {
+
name
+
damage
+
}
+
}
+
weight {
+
minimum
+
maximum
+
}
+
name
__typename
}
}
`);
-
client
-
.query(PokemonQuery, { id: '' })
-
.toPromise()
-
.then(result => {
-
result.data?.pokemon;
+
const Pokemons = () => {
+
const [result] = useQuery({
+
query: PokemonQuery,
+
variables: { id: '' }
});
+
+
// Works
+
console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name)
-
const myQuery = graphql(`
-
query PokemonsAreAwesome {
-
pokemons {
-
id
-
}
-
}
-
`);
+
// Works
+
const { fleeRate } = result.data?.pokemon || {};
+
console.log(fleeRate)
+
// Works
+
const po = result.data?.pokemon;
+
// @ts-expect-error
+
const { pokemon: { weight: { minimum } } } = result.data || {};
+
console.log(po?.name, minimum)
+
+
// Works
+
const { pokemon } = result.data || {};
+
console.log(pokemon?.weight?.maximum)
+
+
return <Pokemon data={result.data?.pokemon} />;
+
}
+
+3 -1
packages/example-external-generator/tsconfig.json
···
"disableTypegen": true,
"shouldCheckForColocatedFragments": false,
"template": "graphql",
-
"templateIsCallExpression": true
+
"templateIsCallExpression": true,
+
"trackFieldUsage": true
}
],
+
"jsx": "react-jsx",
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
/* Modules */
+5 -212
packages/example/src/index.generated.ts
···
import * as Types from '../__generated__/baseGraphQLSP';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-
export type PokQueryVariables = Types.Exact<{
-
limit: Types.Scalars['Int']['input'];
+
export type PoQueryVariables = Types.Exact<{
+
id: Types.Scalars['ID']['input'];
}>;
-
export type PokQuery = {
+
export type PoQuery = {
__typename: 'Query';
-
pokemons?: Array<{
+
pokemon?: {
__typename: 'Pokemon';
id: string;
-
name: string;
fleeRate?: number | null;
-
classification?: string | null;
-
weaknesses?: Array<Types.PokemonType | null> | null;
-
attacks?: {
-
__typename: 'AttacksConnection';
-
fast?: Array<{
-
__typename: 'Attack';
-
damage?: number | null;
-
name?: string | null;
-
} | null> | null;
-
} | null;
-
} | null> | null;
-
};
-
-
export type PokemonFieldsFragment = {
-
__typename: 'Pokemon';
-
id: string;
-
name: string;
-
attacks?: {
-
__typename: 'AttacksConnection';
-
fast?: Array<{
-
__typename: 'Attack';
-
damage?: number | null;
-
name?: string | null;
-
} | null> | null;
} | null;
};
-
export type WeaknessFieldsFragment = {
-
__typename: 'Pokemon';
-
weaknesses?: Array<Types.PokemonType | null> | null;
-
};
-
-
export type PoQueryVariables = Types.Exact<{
-
id: Types.Scalars['ID']['input'];
-
}>;
-
-
export const PokemonFieldsFragmentDoc = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'pokemonFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'attacks' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'fast' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'damage' },
-
},
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
],
-
},
-
},
-
],
-
},
-
},
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
-
export const WeaknessFieldsFragmentDoc = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'weaknessFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<WeaknessFieldsFragment, unknown>;
-
export const PokDocument = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'OperationDefinition',
-
operation: 'query',
-
name: { kind: 'Name', value: 'Pok' },
-
variableDefinitions: [
-
{
-
kind: 'VariableDefinition',
-
variable: {
-
kind: 'Variable',
-
name: { kind: 'Name', value: 'limit' },
-
},
-
type: {
-
kind: 'NonNullType',
-
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } },
-
},
-
},
-
],
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'pokemons' },
-
arguments: [
-
{
-
kind: 'Argument',
-
name: { kind: 'Name', value: 'limit' },
-
value: {
-
kind: 'Variable',
-
name: { kind: 'Name', value: 'limit' },
-
},
-
},
-
],
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
-
{
-
kind: 'FragmentSpread',
-
name: { kind: 'Name', value: 'pokemonFields' },
-
},
-
{
-
kind: 'FragmentSpread',
-
name: { kind: 'Name', value: 'weaknessFields' },
-
},
-
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
-
],
-
},
-
},
-
],
-
},
-
},
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'pokemonFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'attacks' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'fast' },
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{
-
kind: 'Field',
-
name: { kind: 'Name', value: 'damage' },
-
},
-
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
],
-
},
-
},
-
],
-
},
-
},
-
],
-
},
-
},
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'weaknessFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<PokQuery, PokQueryVariables>;
export const PoDocument = {
kind: 'Document',
definitions: [
···
},
},
],
-
} as unknown as DocumentNode<PokQuery, PoQueryVariables>;
+
} as unknown as DocumentNode<PoQuery, PoQueryVariables>;
-29
packages/example/src/index.ts
···
import { gql, createClient } from '@urql/core';
import { Pokemon, PokemonFields, WeakFields } from './Pokemon';
-
const x = gql`
-
query Pok($limit: Int!) {
-
pokemons(limit: $limit) @populate {
-
id
-
name
-
fleeRate
-
classification
-
...pokemonFields
-
...weaknessFields
-
__typename
-
}
-
}
-
-
${PokemonFields}
-
${WeakFields}
-
` as typeof import('./index.generated').PokDocument;
-
-
const client = createClient({
-
url: '',
-
});
-
const PokemonQuery = gql`
query Po($id: ID!) {
pokemon(id: $id) {
···
.then(result => {
result.data?.pokemon;
});
-
-
const myQuery = gql`
-
query PokemonsAreAwesome {
-
pokemons {
-
id
-
}
-
}
-
`;
+25
packages/graphqlsp/README.md
···
- `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
unused fragments and provide a message notifying you about them
+
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
+
unused fields within the same file.
### GraphQL Code Generator client-preset
···
"schema": "./schema.graphql",
"disableTypegen": true,
"templateIsCallExpression": true,
+
"trackFieldUsage": true,
"template": "graphql"
}
]
}
}
```
+
+
## Tracking unused fields
+
+
Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost
+
it will only track in the same file to encourage [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments).
+
Secondly it supports a few patterns which we'll add to as time progresses:
+
+
```ts
+
// Supported cases:
+
const result = (await client.query()) || useFragment();
+
const [result] = useQuery(); // --> urql
+
const { data } = useQuery(); // --> Apollo
+
// Missing cases:
+
const { field } = useFragment(); // can't destructure into your data from the start
+
const [{ data }] = useQuery(); // can't follow array destructuring with object destructuring
+
const {
+
data: { pokemon },
+
} = useQuery(); // can't destructure into your data from the start
+
```
+
+
Lastly we don't track mutations/subscriptions as some folks will add additional fields to properly support
+
normalised cache updates.
## Fragment masking
+302 -2
packages/graphqlsp/src/diagnostics.ts
···
OperationDefinitionNode,
parse,
print,
+
visit,
} from 'graphql';
import { LRUCache } from 'lru-cache';
import fnv1a from '@sindresorhus/fnv1a';
···
findAllCallExpressions,
findAllImports,
findAllTaggedTemplateNodes,
+
findNode,
getSource,
isFileDirty,
} from './ast';
import { resolveTemplate } from './ast/resolve';
import { generateTypedDocumentNodes } from './graphql/generateTypes';
+
import { Logger } from '.';
const clientDirectives = new Set([
'populate',
···
export const MISSING_OPERATION_NAME_CODE = 52002;
export const MISSING_FRAGMENT_CODE = 52003;
export const USING_DEPRECATED_FIELD_CODE = 52004;
+
export const UNUSED_FIELD_CODE = 52005;
let isGeneratingTypes = false;
···
let tsDiagnostics: ts.Diagnostic[] = [];
const cacheKey = fnv1a(
isCallExpression
-
? texts.join('-') +
+
? source.getText() +
fragments.map(x => print(x)).join('-') +
schema.version
: texts.join('-') + schema.version
···
messageText: diag.message.split('\n')[0],
}));
-
const importDiagnostics = checkImportsForFragments(source, info);
+
const importDiagnostics = isCallExpression
+
? checkFieldUsageInFile(
+
source,
+
nodes as ts.NoSubstitutionTemplateLiteral[],
+
info
+
)
+
: checkImportsForFragments(source, info);
return [...tsDiagnostics, ...importDiagnostics];
+
};
+
+
const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => {
+
let node: any = start;
+
let counter = 0;
+
while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) {
+
node = node.parent;
+
counter++;
+
}
+
return node;
+
};
+
+
const traverseDestructuring = (
+
node: ts.ObjectBindingPattern,
+
originalWip: Array<string>,
+
allFields: Array<string>,
+
source: ts.SourceFile,
+
info: ts.server.PluginCreateInfo
+
): Array<string> => {
+
const results = [];
+
for (const binding of node.elements) {
+
if (ts.isObjectBindingPattern(binding.name)) {
+
const wip = [...originalWip];
+
if (
+
binding.propertyName &&
+
allFields.includes(binding.propertyName.getText()) &&
+
!originalWip.includes(binding.propertyName.getText())
+
) {
+
wip.push(binding.propertyName.getText());
+
}
+
const traverseResult = traverseDestructuring(
+
binding.name,
+
wip,
+
allFields,
+
source,
+
info
+
);
+
+
results.push(...traverseResult);
+
} else if (ts.isIdentifier(binding.name)) {
+
const wip = [...originalWip];
+
if (
+
binding.propertyName &&
+
allFields.includes(binding.propertyName.getText()) &&
+
!originalWip.includes(binding.propertyName.getText())
+
) {
+
wip.push(binding.propertyName.getText());
+
} else {
+
wip.push(binding.name.getText());
+
}
+
+
const crawlResult = crawlScope(
+
binding.name,
+
wip,
+
allFields,
+
source,
+
info
+
);
+
+
results.push(...crawlResult);
+
}
+
}
+
+
return results;
+
};
+
+
const crawlScope = (
+
node: ts.Identifier | ts.BindingName,
+
originalWip: Array<string>,
+
allFields: Array<string>,
+
source: ts.SourceFile,
+
info: ts.server.PluginCreateInfo
+
): Array<string> => {
+
let results: string[] = [];
+
+
const references = info.languageService.getReferencesAtPosition(
+
source.fileName,
+
node.getStart()
+
);
+
+
if (!references) return results;
+
+
// Go over all the references tied to the result of
+
// accessing our equery and collect them as fully
+
// qualified paths (ideally ending in a leaf-node)
+
results = references.flatMap(ref => {
+
// If we get a reference to a different file we can bail
+
if (ref.fileName !== source.fileName) return [];
+
// We don't want to end back at our document so we narrow
+
// the scope.
+
if (
+
node.getStart() <= ref.textSpan.start &&
+
node.getEnd() >= ref.textSpan.start + ref.textSpan.length
+
)
+
return [];
+
+
let foundRef = findNode(source, ref.textSpan.start);
+
if (!foundRef) return [];
+
+
const pathParts = [...originalWip];
+
// In here we'll start crawling all the accessors of result
+
// and try to determine the total path
+
// - result.data.pokemon.name --> pokemon.name this is the easy route and never accesses
+
// any of the recursive functions
+
// - 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
+
while (
+
ts.isIdentifier(foundRef) ||
+
ts.isPropertyAccessExpression(foundRef) ||
+
ts.isElementAccessExpression(foundRef) ||
+
ts.isVariableDeclaration(foundRef) ||
+
ts.isBinaryExpression(foundRef)
+
) {
+
if (ts.isVariableDeclaration(foundRef)) {
+
if (ts.isIdentifier(foundRef.name)) {
+
// We have already added the paths because of the right-hand expression,
+
// const pokemon = result.data.pokemon --> we have pokemon as our path,
+
// now re-crawling pokemon for all of its accessors should deliver us the usage
+
// patterns... This might get expensive though if we need to perform this deeply.
+
return crawlScope(foundRef.name, pathParts, allFields, source, info);
+
} else if (ts.isObjectBindingPattern(foundRef.name)) {
+
// First we need to traverse the left-hand side of the variable assignment,
+
// this could be tree-like as we could be dealing with
+
// - const { x: { y: z }, a: { b: { c, d }, e: { f } } } = result.data
+
// Which we will need several paths for...
+
// after doing that we need to re-crawl all of the resulting variables
+
// Crawl down until we have either a leaf node or an object/array that can
+
// be recrawled
+
return traverseDestructuring(
+
foundRef.name,
+
pathParts,
+
allFields,
+
source,
+
info
+
);
+
}
+
} else if (
+
ts.isIdentifier(foundRef) &&
+
allFields.includes(foundRef.text) &&
+
!pathParts.includes(foundRef.text)
+
) {
+
pathParts.push(foundRef.text);
+
} else if (
+
ts.isPropertyAccessExpression(foundRef) &&
+
allFields.includes(foundRef.name.text) &&
+
!pathParts.includes(foundRef.name.text)
+
) {
+
pathParts.push(foundRef.name.text);
+
} else if (
+
ts.isElementAccessExpression(foundRef) &&
+
ts.isStringLiteral(foundRef.argumentExpression) &&
+
allFields.includes(foundRef.argumentExpression.text) &&
+
!pathParts.includes(foundRef.argumentExpression.text)
+
) {
+
pathParts.push(foundRef.argumentExpression.text);
+
}
+
+
foundRef = foundRef.parent;
+
}
+
+
return pathParts.join('.');
+
});
+
+
return results;
+
};
+
+
const checkFieldUsageInFile = (
+
source: ts.SourceFile,
+
nodes: ts.NoSubstitutionTemplateLiteral[],
+
info: ts.server.PluginCreateInfo
+
) => {
+
const logger: Logger = (msg: string) =>
+
info.project.projectService.logger.info(`[GraphQLSP] ${msg}`);
+
const diagnostics: ts.Diagnostic[] = [];
+
const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false;
+
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;
+
+
const variableDeclaration = getVariableDeclaration(node);
+
if (!ts.isVariableDeclaration(variableDeclaration)) return;
+
+
const references = info.languageService.getReferencesAtPosition(
+
source.fileName,
+
variableDeclaration.name.getStart()
+
);
+
if (!references) 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;
+
}
+
+
if (!found || !ts.isVariableStatement(found)) return;
+
+
const [output] = found.declarationList.declarations;
+
+
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()
+
// - 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 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.`,
+
});
+
});
+
});
+
});
+
+
return diagnostics;
};
const checkImportsForFragments = (
+95 -9
pnpm-lock.yaml
···
graphql:
specifier: ^16.8.1
version: 16.8.1
+
urql:
+
specifier: ^4.0.6
+
version: 4.0.6(graphql@16.8.1)(react@18.2.0)
devDependencies:
'@0no-co/graphqlsp':
specifier: file:../graphqlsp
···
'@graphql-codegen/client-preset':
specifier: ^4.1.0
version: 4.1.0(graphql@16.8.1)
+
'@types/react':
+
specifier: ^18.2.45
+
version: 18.2.45
ts-node:
specifier: ^10.9.1
version: 10.9.1(@types/node@18.15.11)(typescript@5.3.3)
···
specifier: ^5.3.3
version: 5.3.3
+
test/e2e/fixture-project-unused-fields:
+
dependencies:
+
'@0no-co/graphqlsp':
+
specifier: workspace:*
+
version: link:../../../packages/graphqlsp
+
'@graphql-typed-document-node/core':
+
specifier: ^3.0.0
+
version: 3.2.0(graphql@16.8.1)
+
'@urql/core':
+
specifier: ^4.0.4
+
version: 4.2.2(graphql@16.8.1)
+
graphql:
+
specifier: ^16.0.0
+
version: 16.8.1
+
urql:
+
specifier: ^4.0.4
+
version: 4.0.6(graphql@16.8.1)(react@18.2.0)
+
devDependencies:
+
'@types/react':
+
specifier: 18.2.45
+
version: 18.2.45
+
typescript:
+
specifier: ^5.3.3
+
version: 5.3.3
+
packages:
/@0no-co/graphql.web@1.0.0(graphql@16.8.1):
resolution: {integrity: sha512-JBq2pWyDchE1vVjj/+c4dzZ8stbpew4RrzpZ3vYdn1WJFGHfYg6YIX1fDdMKtSXJJM9FUlsoDOxemr9WMM2p+A==}
+
peerDependencies:
+
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
+
peerDependenciesMeta:
+
graphql:
+
optional: true
+
dependencies:
+
graphql: 16.8.1
+
dev: false
+
+
/@0no-co/graphql.web@1.0.4(graphql@16.8.1):
+
resolution: {integrity: sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
···
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: true
+
/@types/prop-types@15.7.11:
+
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
+
dev: true
+
+
/@types/react@18.2.45:
+
resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==}
+
dependencies:
+
'@types/prop-types': 15.7.11
+
'@types/scheduler': 0.16.8
+
csstype: 3.1.3
+
dev: true
+
+
/@types/scheduler@0.16.8:
+
resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==}
+
dev: true
+
/@types/semver@7.5.4:
resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==}
dev: true
···
dependencies:
'@0no-co/graphql.web': 1.0.0(graphql@16.8.1)
wonka: 6.3.1
+
transitivePeerDependencies:
+
- graphql
+
dev: false
+
+
/@urql/core@4.2.2(graphql@16.8.1):
+
resolution: {integrity: sha512-TP1kheq9bnrEdnVbJqh0g0ZY/wfdpPeAzjiiDK+Tm+Pbi0O1Xdu6+fUJ/wJo5QpHZzkIyya4/AecG63e6scFqQ==}
+
dependencies:
+
'@0no-co/graphql.web': 1.0.4(graphql@16.8.1)
+
wonka: 6.3.4
transitivePeerDependencies:
- graphql
dev: false
···
which: 2.0.2
dev: true
+
/csstype@3.1.3:
+
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
dev: true
+
/csv-generate@3.4.3:
resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==}
dev: true
···
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
-
/fsevents@2.3.2:
-
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
-
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
-
os: [darwin]
-
requiresBuild: true
-
dev: true
-
optional: true
-
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
···
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
+
/react@18.2.0:
+
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
+
engines: {node: '>=0.10.0'}
+
dependencies:
+
loose-envify: 1.4.0
+
dev: false
+
/read-pkg-up@7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'}
···
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
-
fsevents: 2.3.2
+
fsevents: 2.3.3
dev: true
/rollup@4.7.0:
···
resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==}
dev: true
+
/urql@4.0.6(graphql@16.8.1)(react@18.2.0):
+
resolution: {integrity: sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==}
+
peerDependencies:
+
react: '>= 16.8.0'
+
dependencies:
+
'@urql/core': 4.2.2(graphql@16.8.1)
+
react: 18.2.0
+
wonka: 6.3.4
+
transitivePeerDependencies:
+
- graphql
+
dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
···
/wonka@6.3.1:
resolution: {integrity: sha512-nJyGPcjuBiaLFn8QAlrHd+QjV9AlPO7snOWAhgx6aX0nQLMV6Wi0nqfrkmsXIH0efngbDOroOz2QyLnZMF16Hw==}
+
dev: false
+
+
/wonka@6.3.4:
+
resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==}
dev: false
/wrap-ansi@6.2.0:
+3
test/e2e/fixture-project-unused-fields/.vscode/settings.json
···
+
{
+
"typescript.tsdk": "node_modules/typescript/lib"
+
}
+115
test/e2e/fixture-project-unused-fields/__generated__/baseGraphQLSP.ts
···
+
export type Maybe<T> = T | null;
+
export type InputMaybe<T> = Maybe<T>;
+
export type Exact<T extends { [key: string]: unknown }> = {
+
[K in keyof T]: T[K];
+
};
+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]?: Maybe<T[SubKey]>;
+
};
+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]: Maybe<T[SubKey]>;
+
};
+
export type MakeEmpty<
+
T extends { [key: string]: unknown },
+
K extends keyof T
+
> = { [_ in K]?: never };
+
export type Incremental<T> =
+
| T
+
| {
+
[P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never;
+
};
+
/** All built-in and custom scalars, mapped to their actual values */
+
export type Scalars = {
+
ID: { input: string; output: string };
+
String: { input: string; output: string };
+
Boolean: { input: boolean; output: boolean };
+
Int: { input: number; output: number };
+
Float: { input: number; output: number };
+
};
+
+
/** Move a Pokémon can perform with the associated damage and type. */
+
export type Attack = {
+
__typename: 'Attack';
+
damage?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
type?: Maybe<PokemonType>;
+
};
+
+
export type AttacksConnection = {
+
__typename: 'AttacksConnection';
+
fast?: Maybe<Array<Maybe<Attack>>>;
+
special?: Maybe<Array<Maybe<Attack>>>;
+
};
+
+
/** Requirement that prevents an evolution through regular means of levelling up. */
+
export type EvolutionRequirement = {
+
__typename: 'EvolutionRequirement';
+
amount?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
};
+
+
export type Pokemon = {
+
__typename: 'Pokemon';
+
attacks?: Maybe<AttacksConnection>;
+
/** @deprecated And this is the reason why */
+
classification?: Maybe<Scalars['String']['output']>;
+
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
+
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
+
/** Likelihood of an attempt to catch a Pokémon to fail. */
+
fleeRate?: Maybe<Scalars['Float']['output']>;
+
height?: Maybe<PokemonDimension>;
+
id: Scalars['ID']['output'];
+
/** Maximum combat power a Pokémon may achieve at max level. */
+
maxCP?: Maybe<Scalars['Int']['output']>;
+
/** Maximum health points a Pokémon may achieve at max level. */
+
maxHP?: Maybe<Scalars['Int']['output']>;
+
name: Scalars['String']['output'];
+
resistant?: Maybe<Array<Maybe<PokemonType>>>;
+
types?: Maybe<Array<Maybe<PokemonType>>>;
+
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
+
weight?: Maybe<PokemonDimension>;
+
};
+
+
export type PokemonDimension = {
+
__typename: 'PokemonDimension';
+
maximum?: Maybe<Scalars['String']['output']>;
+
minimum?: Maybe<Scalars['String']['output']>;
+
};
+
+
/** Elemental property associated with either a Pokémon or one of their moves. */
+
export type PokemonType =
+
| 'Bug'
+
| 'Dark'
+
| 'Dragon'
+
| 'Electric'
+
| 'Fairy'
+
| 'Fighting'
+
| 'Fire'
+
| 'Flying'
+
| 'Ghost'
+
| 'Grass'
+
| 'Ground'
+
| 'Ice'
+
| 'Normal'
+
| 'Poison'
+
| 'Psychic'
+
| 'Rock'
+
| 'Steel'
+
| 'Water';
+
+
export type Query = {
+
__typename: 'Query';
+
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
+
pokemon?: Maybe<Pokemon>;
+
/** List out all Pokémon, optionally in pages */
+
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
+
};
+
+
export type QueryPokemonArgs = {
+
id: Scalars['ID']['input'];
+
};
+
+
export type QueryPokemonsArgs = {
+
limit?: InputMaybe<Scalars['Int']['input']>;
+
skip?: InputMaybe<Scalars['Int']['input']>;
+
};
+49
test/e2e/fixture-project-unused-fields/fixtures/destructuring.tsx
···
+
import { useQuery } from 'urql';
+
import { graphql } from './gql';
+
// @ts-expect-error
+
import { Pokemon } from './fragment';
+
import * as React from 'react';
+
+
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: '' }
+
});
+
+
// Works
+
const { fleeRate } = result.data?.pokemon || {};
+
console.log(fleeRate)
+
// @ts-expect-error
+
const { pokemon: { weight: { minimum } } } = result.data || {};
+
console.log(minimum)
+
+
// Works
+
const { pokemon } = result.data || {};
+
console.log(pokemon?.weight?.maximum)
+
+
// @ts-expect-error
+
return <Pokemon data={result.data?.pokemon} />;
+
}
+
+27
test/e2e/fixture-project-unused-fields/fixtures/fragment-destructuring.tsx
···
+
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+
import { graphql } from './gql';
+
+
export const PokemonFields = graphql(`
+
fragment pokemonFields on Pokemon {
+
id
+
name
+
attacks {
+
fast {
+
damage
+
name
+
}
+
}
+
}
+
`)
+
+
export const Pokemon = (data: any) => {
+
const { name } = useFragment(PokemonFields, data);
+
return `hi ${name}`;
+
};
+
+
export function useFragment<Type>(
+
_fragment: TypedDocumentNode<Type>,
+
data: any
+
): Type {
+
return data;
+
}
+27
test/e2e/fixture-project-unused-fields/fixtures/fragment.tsx
···
+
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+
import { graphql } from './gql';
+
+
export const PokemonFields = graphql(`
+
fragment pokemonFields on Pokemon {
+
id
+
name
+
attacks {
+
fast {
+
damage
+
name
+
}
+
}
+
}
+
`)
+
+
export const Pokemon = (data: any) => {
+
const pokemon = useFragment(PokemonFields, data);
+
return `hi ${pokemon.name}`;
+
};
+
+
export function useFragment<Type>(
+
_fragment: TypedDocumentNode<Type>,
+
data: any
+
): Type {
+
return data;
+
}
+85
test/e2e/fixture-project-unused-fields/fixtures/gql/fragment-masking.ts
···
+
import {
+
ResultOf,
+
DocumentTypeDecoration,
+
TypedDocumentNode,
+
} from '@graphql-typed-document-node/core';
+
import { FragmentDefinitionNode } from 'graphql';
+
import { Incremental } from './graphql';
+
+
export type FragmentType<
+
TDocumentType extends DocumentTypeDecoration<any, any>
+
> = TDocumentType extends DocumentTypeDecoration<infer TType, any>
+
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
+
? TKey extends string
+
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
+
: never
+
: never
+
: never;
+
+
// return non-nullable if `fragmentType` is non-nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
+
): TType;
+
// return nullable if `fragmentType` is nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| FragmentType<DocumentTypeDecoration<TType, any>>
+
| null
+
| undefined
+
): TType | null | undefined;
+
// return array of non-nullable if `fragmentType` is array of non-nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
): ReadonlyArray<TType>;
+
// return array of nullable if `fragmentType` is array of nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
| null
+
| undefined
+
): ReadonlyArray<TType> | null | undefined;
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| FragmentType<DocumentTypeDecoration<TType, any>>
+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
| null
+
| undefined
+
): TType | ReadonlyArray<TType> | null | undefined {
+
return fragmentType as any;
+
}
+
+
export function makeFragmentData<
+
F extends DocumentTypeDecoration<any, any>,
+
FT extends ResultOf<F>
+
>(data: FT, _fragment: F): FragmentType<F> {
+
return data as FragmentType<F>;
+
}
+
export function isFragmentReady<TQuery, TFrag>(
+
queryNode: DocumentTypeDecoration<TQuery, any>,
+
fragmentNode: TypedDocumentNode<TFrag>,
+
data:
+
| FragmentType<TypedDocumentNode<Incremental<TFrag>, any>>
+
| null
+
| undefined
+
): data is FragmentType<typeof fragmentNode> {
+
const deferredFields = (
+
queryNode as {
+
__meta__?: { deferredFields: Record<string, (keyof TFrag)[]> };
+
}
+
).__meta__?.deferredFields;
+
+
if (!deferredFields) return true;
+
+
const fragDef = fragmentNode.definitions[0] as
+
| FragmentDefinitionNode
+
| undefined;
+
const fragName = fragDef?.name?.value;
+
+
const fields = (fragName && deferredFields[fragName]) || [];
+
return fields.length > 0 && fields.every(field => data && field in data);
+
}
+54
test/e2e/fixture-project-unused-fields/fixtures/gql/gql.ts
···
+
/* eslint-disable */
+
import * as types from './graphql';
+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
+
/**
+
* Map of all GraphQL operations in the project.
+
*
+
* This map has several performance disadvantages:
+
* 1. It is not tree-shakeable, so it will include all operations in the project.
+
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
+
* 3. It does not support dead code elimination, so it will add unused operations.
+
*
+
* Therefore it is highly recommended to use the babel or swc plugin for production.
+
*/
+
const documents = {
+
'\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n':
+
types.PokemonFieldsFragmentDoc,
+
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n':
+
types.PoDocument,
+
};
+
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*
+
*
+
* @example
+
* ```ts
+
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
+
* ```
+
*
+
* The query argument is unknown!
+
* Please regenerate the types.
+
*/
+
export function graphql(source: string): unknown;
+
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*/
+
export function graphql(
+
source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n'
+
): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n'];
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*/
+
export function graphql(
+
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'
+
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'];
+
+
export function graphql(source: string) {
+
return (documents as any)[source] ?? {};
+
}
+
+
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
+
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
+340
test/e2e/fixture-project-unused-fields/fixtures/gql/graphql.ts
···
+
/* eslint-disable */
+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
export type Maybe<T> = T | null;
+
export type InputMaybe<T> = Maybe<T>;
+
export type Exact<T extends { [key: string]: unknown }> = {
+
[K in keyof T]: T[K];
+
};
+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]?: Maybe<T[SubKey]>;
+
};
+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]: Maybe<T[SubKey]>;
+
};
+
export type MakeEmpty<
+
T extends { [key: string]: unknown },
+
K extends keyof T
+
> = { [_ in K]?: never };
+
export type Incremental<T> =
+
| T
+
| {
+
[P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never;
+
};
+
/** All built-in and custom scalars, mapped to their actual values */
+
export type Scalars = {
+
ID: { input: string; output: string };
+
String: { input: string; output: string };
+
Boolean: { input: boolean; output: boolean };
+
Int: { input: number; output: number };
+
Float: { input: number; output: number };
+
};
+
+
/** Move a Pokémon can perform with the associated damage and type. */
+
export type Attack = {
+
__typename?: 'Attack';
+
damage?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
type?: Maybe<PokemonType>;
+
};
+
+
export type AttacksConnection = {
+
__typename?: 'AttacksConnection';
+
fast?: Maybe<Array<Maybe<Attack>>>;
+
special?: Maybe<Array<Maybe<Attack>>>;
+
};
+
+
/** Requirement that prevents an evolution through regular means of levelling up. */
+
export type EvolutionRequirement = {
+
__typename?: 'EvolutionRequirement';
+
amount?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
};
+
+
export type Pokemon = {
+
__typename?: 'Pokemon';
+
attacks?: Maybe<AttacksConnection>;
+
/** @deprecated And this is the reason why */
+
classification?: Maybe<Scalars['String']['output']>;
+
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
+
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
+
/** Likelihood of an attempt to catch a Pokémon to fail. */
+
fleeRate?: Maybe<Scalars['Float']['output']>;
+
height?: Maybe<PokemonDimension>;
+
id: Scalars['ID']['output'];
+
/** Maximum combat power a Pokémon may achieve at max level. */
+
maxCP?: Maybe<Scalars['Int']['output']>;
+
/** Maximum health points a Pokémon may achieve at max level. */
+
maxHP?: Maybe<Scalars['Int']['output']>;
+
name: Scalars['String']['output'];
+
resistant?: Maybe<Array<Maybe<PokemonType>>>;
+
types?: Maybe<Array<Maybe<PokemonType>>>;
+
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
+
weight?: Maybe<PokemonDimension>;
+
};
+
+
export type PokemonDimension = {
+
__typename?: 'PokemonDimension';
+
maximum?: Maybe<Scalars['String']['output']>;
+
minimum?: Maybe<Scalars['String']['output']>;
+
};
+
+
/** Elemental property associated with either a Pokémon or one of their moves. */
+
export enum PokemonType {
+
Bug = 'Bug',
+
Dark = 'Dark',
+
Dragon = 'Dragon',
+
Electric = 'Electric',
+
Fairy = 'Fairy',
+
Fighting = 'Fighting',
+
Fire = 'Fire',
+
Flying = 'Flying',
+
Ghost = 'Ghost',
+
Grass = 'Grass',
+
Ground = 'Ground',
+
Ice = 'Ice',
+
Normal = 'Normal',
+
Poison = 'Poison',
+
Psychic = 'Psychic',
+
Rock = 'Rock',
+
Steel = 'Steel',
+
Water = 'Water',
+
}
+
+
export type Query = {
+
__typename?: 'Query';
+
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
+
pokemon?: Maybe<Pokemon>;
+
/** List out all Pokémon, optionally in pages */
+
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
+
};
+
+
export type QueryPokemonArgs = {
+
id: Scalars['ID']['input'];
+
};
+
+
export type QueryPokemonsArgs = {
+
limit?: InputMaybe<Scalars['Int']['input']>;
+
skip?: InputMaybe<Scalars['Int']['input']>;
+
};
+
+
export type PokemonFieldsFragment = {
+
__typename?: 'Pokemon';
+
id: string;
+
name: string;
+
attacks?: {
+
__typename?: 'AttacksConnection';
+
fast?: Array<{
+
__typename?: 'Attack';
+
damage?: number | null;
+
name?: string | null;
+
} | null> | null;
+
} | null;
+
} & { ' $fragmentName'?: 'PokemonFieldsFragment' };
+
+
export type PoQueryVariables = Exact<{
+
id: Scalars['ID']['input'];
+
}>;
+
+
export type PoQuery = {
+
__typename?: 'Query';
+
pokemon?:
+
| ({
+
__typename: 'Pokemon';
+
id: string;
+
fleeRate?: number | null;
+
name: string;
+
attacks?: {
+
__typename?: 'AttacksConnection';
+
special?: Array<{
+
__typename?: 'Attack';
+
name?: string | null;
+
damage?: number | null;
+
} | null> | null;
+
} | null;
+
weight?: {
+
__typename?: 'PokemonDimension';
+
minimum?: string | null;
+
maximum?: string | null;
+
} | null;
+
} & {
+
' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment };
+
})
+
| null;
+
};
+
+
export const PokemonFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'fast' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+
export const PoDocument = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'OperationDefinition',
+
operation: 'query',
+
name: { kind: 'Name', value: 'Po' },
+
variableDefinitions: [
+
{
+
kind: 'VariableDefinition',
+
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+
type: {
+
kind: 'NonNullType',
+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
+
},
+
},
+
],
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'pokemon' },
+
arguments: [
+
{
+
kind: 'Argument',
+
name: { kind: 'Name', value: 'id' },
+
value: {
+
kind: 'Variable',
+
name: { kind: 'Name', value: 'id' },
+
},
+
},
+
],
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
+
{
+
kind: 'FragmentSpread',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'special' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'name' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'weight' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'minimum' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'maximum' },
+
},
+
],
+
},
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'fast' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<PoQuery, PoQueryVariables>;
+2
test/e2e/fixture-project-unused-fields/fixtures/gql/index.ts
···
+
export * from './fragment-masking';
+
export * from './gql';
+39
test/e2e/fixture-project-unused-fields/fixtures/immediate-destructuring.tsx
···
+
import { useQuery } from 'urql';
+
import { graphql } from './gql';
+
// @ts-expect-error
+
import { Pokemon } from './fragment';
+
import * as React from 'react';
+
+
const PokemonQuery = graphql(`
+
query Po($id: ID!) {
+
pokemon(id: $id) {
+
id
+
fleeRate
+
...pokemonFields
+
attacks {
+
special {
+
name
+
damage
+
}
+
}
+
weight {
+
minimum
+
maximum
+
}
+
name
+
__typename
+
}
+
}
+
`);
+
+
const Pokemons = () => {
+
// @ts-expect-error
+
const [{ data: { pokemon: { fleeRate, weight: { minimum, maximum } } } }] = useQuery({
+
query: PokemonQuery,
+
variables: { id: '' }
+
});
+
+
// @ts-expect-error
+
return <Pokemon data={{ fleeRate, weight: { minimum, maximum } }} />;
+
}
+
+42
test/e2e/fixture-project-unused-fields/fixtures/property-access.tsx
···
+
import { useQuery } from 'urql';
+
import { graphql } from './gql';
+
// @ts-expect-error
+
import { Pokemon } from './fragment';
+
import * as React from 'react';
+
+
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 = result.data?.pokemon
+
console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name)
+
console.log(pokemon?.name)
+
+
// @ts-expect-error
+
return <Pokemon data={result.data?.pokemon} />;
+
}
+
+85
test/e2e/fixture-project-unused-fields/gql/fragment-masking.ts
···
+
import {
+
ResultOf,
+
DocumentTypeDecoration,
+
TypedDocumentNode,
+
} from '@graphql-typed-document-node/core';
+
import { FragmentDefinitionNode } from 'graphql';
+
import { Incremental } from './graphql';
+
+
export type FragmentType<
+
TDocumentType extends DocumentTypeDecoration<any, any>
+
> = TDocumentType extends DocumentTypeDecoration<infer TType, any>
+
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
+
? TKey extends string
+
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
+
: never
+
: never
+
: never;
+
+
// return non-nullable if `fragmentType` is non-nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
+
): TType;
+
// return nullable if `fragmentType` is nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| FragmentType<DocumentTypeDecoration<TType, any>>
+
| null
+
| undefined
+
): TType | null | undefined;
+
// return array of non-nullable if `fragmentType` is array of non-nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
): ReadonlyArray<TType>;
+
// return array of nullable if `fragmentType` is array of nullable
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
| null
+
| undefined
+
): ReadonlyArray<TType> | null | undefined;
+
export function useFragment<TType>(
+
_documentNode: DocumentTypeDecoration<TType, any>,
+
fragmentType:
+
| FragmentType<DocumentTypeDecoration<TType, any>>
+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
+
| null
+
| undefined
+
): TType | ReadonlyArray<TType> | null | undefined {
+
return fragmentType as any;
+
}
+
+
export function makeFragmentData<
+
F extends DocumentTypeDecoration<any, any>,
+
FT extends ResultOf<F>
+
>(data: FT, _fragment: F): FragmentType<F> {
+
return data as FragmentType<F>;
+
}
+
export function isFragmentReady<TQuery, TFrag>(
+
queryNode: DocumentTypeDecoration<TQuery, any>,
+
fragmentNode: TypedDocumentNode<TFrag>,
+
data:
+
| FragmentType<TypedDocumentNode<Incremental<TFrag>, any>>
+
| null
+
| undefined
+
): data is FragmentType<typeof fragmentNode> {
+
const deferredFields = (
+
queryNode as {
+
__meta__?: { deferredFields: Record<string, (keyof TFrag)[]> };
+
}
+
).__meta__?.deferredFields;
+
+
if (!deferredFields) return true;
+
+
const fragDef = fragmentNode.definitions[0] as
+
| FragmentDefinitionNode
+
| undefined;
+
const fragName = fragDef?.name?.value;
+
+
const fields = (fragName && deferredFields[fragName]) || [];
+
return fields.length > 0 && fields.every(field => data && field in data);
+
}
+54
test/e2e/fixture-project-unused-fields/gql/gql.ts
···
+
/* eslint-disable */
+
import * as types from './graphql';
+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
+
/**
+
* Map of all GraphQL operations in the project.
+
*
+
* This map has several performance disadvantages:
+
* 1. It is not tree-shakeable, so it will include all operations in the project.
+
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
+
* 3. It does not support dead code elimination, so it will add unused operations.
+
*
+
* Therefore it is highly recommended to use the babel or swc plugin for production.
+
*/
+
const documents = {
+
'\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n':
+
types.PokemonFieldsFragmentDoc,
+
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n':
+
types.PoDocument,
+
};
+
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*
+
*
+
* @example
+
* ```ts
+
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
+
* ```
+
*
+
* The query argument is unknown!
+
* Please regenerate the types.
+
*/
+
export function graphql(source: string): unknown;
+
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*/
+
export function graphql(
+
source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n'
+
): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n'];
+
/**
+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+
*/
+
export function graphql(
+
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'
+
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'];
+
+
export function graphql(source: string) {
+
return (documents as any)[source] ?? {};
+
}
+
+
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
+
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
+340
test/e2e/fixture-project-unused-fields/gql/graphql.ts
···
+
/* eslint-disable */
+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
export type Maybe<T> = T | null;
+
export type InputMaybe<T> = Maybe<T>;
+
export type Exact<T extends { [key: string]: unknown }> = {
+
[K in keyof T]: T[K];
+
};
+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]?: Maybe<T[SubKey]>;
+
};
+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]: Maybe<T[SubKey]>;
+
};
+
export type MakeEmpty<
+
T extends { [key: string]: unknown },
+
K extends keyof T
+
> = { [_ in K]?: never };
+
export type Incremental<T> =
+
| T
+
| {
+
[P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never;
+
};
+
/** All built-in and custom scalars, mapped to their actual values */
+
export type Scalars = {
+
ID: { input: string; output: string };
+
String: { input: string; output: string };
+
Boolean: { input: boolean; output: boolean };
+
Int: { input: number; output: number };
+
Float: { input: number; output: number };
+
};
+
+
/** Move a Pokémon can perform with the associated damage and type. */
+
export type Attack = {
+
__typename?: 'Attack';
+
damage?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
type?: Maybe<PokemonType>;
+
};
+
+
export type AttacksConnection = {
+
__typename?: 'AttacksConnection';
+
fast?: Maybe<Array<Maybe<Attack>>>;
+
special?: Maybe<Array<Maybe<Attack>>>;
+
};
+
+
/** Requirement that prevents an evolution through regular means of levelling up. */
+
export type EvolutionRequirement = {
+
__typename?: 'EvolutionRequirement';
+
amount?: Maybe<Scalars['Int']['output']>;
+
name?: Maybe<Scalars['String']['output']>;
+
};
+
+
export type Pokemon = {
+
__typename?: 'Pokemon';
+
attacks?: Maybe<AttacksConnection>;
+
/** @deprecated And this is the reason why */
+
classification?: Maybe<Scalars['String']['output']>;
+
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
+
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
+
/** Likelihood of an attempt to catch a Pokémon to fail. */
+
fleeRate?: Maybe<Scalars['Float']['output']>;
+
height?: Maybe<PokemonDimension>;
+
id: Scalars['ID']['output'];
+
/** Maximum combat power a Pokémon may achieve at max level. */
+
maxCP?: Maybe<Scalars['Int']['output']>;
+
/** Maximum health points a Pokémon may achieve at max level. */
+
maxHP?: Maybe<Scalars['Int']['output']>;
+
name: Scalars['String']['output'];
+
resistant?: Maybe<Array<Maybe<PokemonType>>>;
+
types?: Maybe<Array<Maybe<PokemonType>>>;
+
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
+
weight?: Maybe<PokemonDimension>;
+
};
+
+
export type PokemonDimension = {
+
__typename?: 'PokemonDimension';
+
maximum?: Maybe<Scalars['String']['output']>;
+
minimum?: Maybe<Scalars['String']['output']>;
+
};
+
+
/** Elemental property associated with either a Pokémon or one of their moves. */
+
export enum PokemonType {
+
Bug = 'Bug',
+
Dark = 'Dark',
+
Dragon = 'Dragon',
+
Electric = 'Electric',
+
Fairy = 'Fairy',
+
Fighting = 'Fighting',
+
Fire = 'Fire',
+
Flying = 'Flying',
+
Ghost = 'Ghost',
+
Grass = 'Grass',
+
Ground = 'Ground',
+
Ice = 'Ice',
+
Normal = 'Normal',
+
Poison = 'Poison',
+
Psychic = 'Psychic',
+
Rock = 'Rock',
+
Steel = 'Steel',
+
Water = 'Water',
+
}
+
+
export type Query = {
+
__typename?: 'Query';
+
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
+
pokemon?: Maybe<Pokemon>;
+
/** List out all Pokémon, optionally in pages */
+
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
+
};
+
+
export type QueryPokemonArgs = {
+
id: Scalars['ID']['input'];
+
};
+
+
export type QueryPokemonsArgs = {
+
limit?: InputMaybe<Scalars['Int']['input']>;
+
skip?: InputMaybe<Scalars['Int']['input']>;
+
};
+
+
export type PokemonFieldsFragment = {
+
__typename?: 'Pokemon';
+
id: string;
+
name: string;
+
attacks?: {
+
__typename?: 'AttacksConnection';
+
fast?: Array<{
+
__typename?: 'Attack';
+
damage?: number | null;
+
name?: string | null;
+
} | null> | null;
+
} | null;
+
} & { ' $fragmentName'?: 'PokemonFieldsFragment' };
+
+
export type PoQueryVariables = Exact<{
+
id: Scalars['ID']['input'];
+
}>;
+
+
export type PoQuery = {
+
__typename?: 'Query';
+
pokemon?:
+
| ({
+
__typename: 'Pokemon';
+
id: string;
+
fleeRate?: number | null;
+
name: string;
+
attacks?: {
+
__typename?: 'AttacksConnection';
+
special?: Array<{
+
__typename?: 'Attack';
+
name?: string | null;
+
damage?: number | null;
+
} | null> | null;
+
} | null;
+
weight?: {
+
__typename?: 'PokemonDimension';
+
minimum?: string | null;
+
maximum?: string | null;
+
} | null;
+
} & {
+
' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment };
+
})
+
| null;
+
};
+
+
export const PokemonFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'fast' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+
export const PoDocument = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'OperationDefinition',
+
operation: 'query',
+
name: { kind: 'Name', value: 'Po' },
+
variableDefinitions: [
+
{
+
kind: 'VariableDefinition',
+
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+
type: {
+
kind: 'NonNullType',
+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
+
},
+
},
+
],
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'pokemon' },
+
arguments: [
+
{
+
kind: 'Argument',
+
name: { kind: 'Name', value: 'id' },
+
value: {
+
kind: 'Variable',
+
name: { kind: 'Name', value: 'id' },
+
},
+
},
+
],
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
+
{
+
kind: 'FragmentSpread',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'special' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'name' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'weight' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'minimum' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'maximum' },
+
},
+
],
+
},
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'fast' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<PoQuery, PoQueryVariables>;
+2
test/e2e/fixture-project-unused-fields/gql/index.ts
···
+
export * from './fragment-masking';
+
export * from './gql';
+15
test/e2e/fixture-project-unused-fields/package.json
···
+
{
+
"name": "fixtures",
+
"private": true,
+
"dependencies": {
+
"graphql": "^16.0.0",
+
"@graphql-typed-document-node/core": "^3.0.0",
+
"@0no-co/graphqlsp": "workspace:*",
+
"@urql/core": "^4.0.4",
+
"urql": "^4.0.4"
+
},
+
"devDependencies": {
+
"@types/react": "18.2.45",
+
"typescript": "^5.3.3"
+
}
+
}
+94
test/e2e/fixture-project-unused-fields/schema.graphql
···
+
### This file was generated by Nexus Schema
+
### Do not make changes to this file directly
+
+
"""
+
Move a Pokémon can perform with the associated damage and type.
+
"""
+
type Attack {
+
damage: Int
+
name: String
+
type: PokemonType
+
}
+
+
type AttacksConnection {
+
fast: [Attack]
+
special: [Attack]
+
}
+
+
"""
+
Requirement that prevents an evolution through regular means of levelling up.
+
"""
+
type EvolutionRequirement {
+
amount: Int
+
name: String
+
}
+
+
type Pokemon {
+
attacks: AttacksConnection
+
classification: String @deprecated(reason: "And this is the reason why")
+
evolutionRequirements: [EvolutionRequirement]
+
evolutions: [Pokemon]
+
+
"""
+
Likelihood of an attempt to catch a Pokémon to fail.
+
"""
+
fleeRate: Float
+
height: PokemonDimension
+
id: ID!
+
+
"""
+
Maximum combat power a Pokémon may achieve at max level.
+
"""
+
maxCP: Int
+
+
"""
+
Maximum health points a Pokémon may achieve at max level.
+
"""
+
maxHP: Int
+
name: String!
+
resistant: [PokemonType]
+
types: [PokemonType]
+
weaknesses: [PokemonType]
+
weight: PokemonDimension
+
}
+
+
type PokemonDimension {
+
maximum: String
+
minimum: String
+
}
+
+
"""
+
Elemental property associated with either a Pokémon or one of their moves.
+
"""
+
enum PokemonType {
+
Bug
+
Dark
+
Dragon
+
Electric
+
Fairy
+
Fighting
+
Fire
+
Flying
+
Ghost
+
Grass
+
Ground
+
Ice
+
Normal
+
Poison
+
Psychic
+
Rock
+
Steel
+
Water
+
}
+
+
type Query {
+
"""
+
Get a single Pokémon by its ID, a three character long identifier padded with zeroes
+
"""
+
pokemon(id: ID!): Pokemon
+
+
"""
+
List out all Pokémon, optionally in pages
+
"""
+
pokemons(limit: Int, skip: Int): [Pokemon]
+
}
+23
test/e2e/fixture-project-unused-fields/tsconfig.json
···
+
{
+
"compilerOptions": {
+
"plugins": [
+
{
+
"name": "@0no-co/graphqlsp",
+
"schema": "./schema.graphql",
+
"disableTypegen": true,
+
"trackFieldUsage": true,
+
"shouldCheckForColocatedFragments": false,
+
"template": "graphql",
+
"templateIsCallExpression": true
+
}
+
],
+
"target": "es2016",
+
"jsx": "react-jsx",
+
"esModuleInterop": true,
+
"moduleResolution": "node",
+
"forceConsistentCasingInFileNames": true,
+
"strict": true,
+
"skipLibCheck": true
+
},
+
"exclude": ["node_modules", "fixtures"]
+
}
+429
test/e2e/unused-fieds.test.ts
···
+
import { expect, afterAll, beforeAll, it, describe } from 'vitest';
+
import { TSServer } from './server';
+
import path from 'node:path';
+
import fs from 'node:fs';
+
import url from 'node:url';
+
import ts from 'typescript/lib/tsserverlibrary';
+
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+
const projectPath = path.resolve(__dirname, 'fixture-project-unused-fields');
+
describe('unused fields', () => {
+
const outfileDestructuringFromStart = path.join(
+
projectPath,
+
'immediate-destructuring.tsx'
+
);
+
const outfileDestructuring = path.join(projectPath, 'destructuring.tsx');
+
const outfileFragmentDestructuring = path.join(
+
projectPath,
+
'fragment-destructuring.tsx'
+
);
+
const outfileFragment = path.join(projectPath, 'fragment.tsx');
+
const outfilePropAccess = path.join(projectPath, 'property-access.tsx');
+
+
let server: TSServer;
+
beforeAll(async () => {
+
server = new TSServer(projectPath, { debugLog: false });
+
+
server.sendCommand('open', {
+
file: outfileDestructuring,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
server.sendCommand('open', {
+
file: outfileFragment,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
server.sendCommand('open', {
+
file: outfilePropAccess,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
server.sendCommand('open', {
+
file: outfileFragmentDestructuring,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
server.sendCommand('open', {
+
file: outfileDestructuringFromStart,
+
fileContent: '// empty',
+
scriptKindName: 'TS',
+
} satisfies ts.server.protocol.OpenRequestArgs);
+
+
server.sendCommand('updateOpen', {
+
openFiles: [
+
{
+
file: outfileDestructuring,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/destructuring.tsx'),
+
'utf-8'
+
),
+
},
+
{
+
file: outfileFragment,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/fragment.tsx'),
+
'utf-8'
+
),
+
},
+
{
+
file: outfilePropAccess,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/property-access.tsx'),
+
'utf-8'
+
),
+
},
+
{
+
file: outfileDestructuringFromStart,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/immediate-destructuring.tsx'),
+
'utf-8'
+
),
+
},
+
{
+
file: outfileFragmentDestructuring,
+
fileContent: fs.readFileSync(
+
path.join(projectPath, 'fixtures/fragment-destructuring.tsx'),
+
'utf-8'
+
),
+
},
+
],
+
} satisfies ts.server.protocol.UpdateOpenRequestArgs);
+
+
server.sendCommand('saveto', {
+
file: outfileDestructuring,
+
tmpfile: outfileDestructuring,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
+
server.sendCommand('saveto', {
+
file: outfileFragment,
+
tmpfile: outfileFragment,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
+
server.sendCommand('saveto', {
+
file: outfilePropAccess,
+
tmpfile: outfilePropAccess,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
+
server.sendCommand('saveto', {
+
file: outfileFragmentDestructuring,
+
tmpfile: outfileFragmentDestructuring,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
+
server.sendCommand('saveto', {
+
file: outfileDestructuringFromStart,
+
tmpfile: outfileDestructuringFromStart,
+
} satisfies ts.server.protocol.SavetoRequestArgs);
+
});
+
+
afterAll(() => {
+
try {
+
fs.unlinkSync(outfileDestructuring);
+
fs.unlinkSync(outfileFragment);
+
fs.unlinkSync(outfilePropAccess);
+
fs.unlinkSync(outfileFragmentDestructuring);
+
fs.unlinkSync(outfileDestructuringFromStart);
+
} catch {}
+
});
+
+
it('gives unused fields with fragments', async () => {
+
await server.waitForResponse(
+
e =>
+
e.type === 'event' &&
+
e.event === 'semanticDiag' &&
+
e.body?.file === outfileFragment
+
);
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfileFragment
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"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.",
+
},
+
]
+
`);
+
}, 30000);
+
+
it('gives unused fields with fragments destructuring', async () => {
+
await server.waitForResponse(
+
e =>
+
e.type === 'event' &&
+
e.event === 'semanticDiag' &&
+
e.body?.file === outfileFragmentDestructuring
+
);
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfileFragmentDestructuring
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"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.",
+
},
+
]
+
`);
+
}, 30000);
+
+
it('gives semantc diagnostics with property access', async () => {
+
await server.waitForResponse(
+
e =>
+
e.type === 'event' &&
+
e.event === 'semanticDiag' &&
+
e.body?.file === outfilePropAccess
+
);
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfilePropAccess
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"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",
+
"code": 2578,
+
"end": {
+
"line": 3,
+
"offset": 20,
+
},
+
"start": {
+
"line": 3,
+
"offset": 1,
+
},
+
"text": "Unused '@ts-expect-error' directive.",
+
},
+
]
+
`);
+
}, 30000);
+
+
it('gives unused fields with destructuring', async () => {
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfileDestructuring
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"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",
+
"code": 2578,
+
"end": {
+
"line": 3,
+
"offset": 20,
+
},
+
"start": {
+
"line": 3,
+
"offset": 1,
+
},
+
"text": "Unused '@ts-expect-error' directive.",
+
},
+
]
+
`);
+
}, 30000);
+
+
it('gives unused fields with immedaite destructuring', async () => {
+
const res = server.responses.filter(
+
resp =>
+
resp.type === 'event' &&
+
resp.event === 'semanticDiag' &&
+
resp.body?.file === outfileDestructuringFromStart
+
);
+
expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
+
[
+
{
+
"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",
+
"code": 2578,
+
"end": {
+
"line": 3,
+
"offset": 20,
+
},
+
"start": {
+
"line": 3,
+
"offset": 1,
+
},
+
"text": "Unused '@ts-expect-error' directive.",
+
},
+
]
+
`);
+
}, 30000);
+
});