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

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Jovi De Croock
Mateusz Burzyński
and committed by
GitHub
a2180af4 d8f1ba88

+2397 -480
+5
.changeset/three-weeks-taste.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Track field usage and warn when a field goes unused
+25
README.md
··· 68 68 - `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions 69 69 - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 70 70 unused fragments and provide a message notifying you about them 71 + - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 72 + unused fields within the same file. 71 73 72 74 ### GraphQL Code Generator client-preset 73 75 ··· 82 84 "schema": "./schema.graphql", 83 85 "disableTypegen": true, 84 86 "templateIsCallExpression": true, 87 + "trackFieldUsage": true, 85 88 "template": "graphql" 86 89 } 87 90 ] 88 91 } 89 92 } 90 93 ``` 94 + 95 + ## Tracking unused fields 96 + 97 + Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost 98 + it will only track in the same file to encourage [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments). 99 + Secondly it supports a few patterns which we'll add to as time progresses: 100 + 101 + ```ts 102 + // Supported cases: 103 + const result = (await client.query()) || useFragment(); 104 + const [result] = useQuery(); // --> urql 105 + const { data } = useQuery(); // --> Apollo 106 + // Missing cases: 107 + const { field } = useFragment(); // can't destructure into your data from the start 108 + const [{ data }] = useQuery(); // can't follow array destructuring with object destructuring 109 + const { 110 + data: { pokemon }, 111 + } = useQuery(); // can't destructure into your data from the start 112 + ``` 113 + 114 + Lastly we don't track mutations/subscriptions as some folks will add additional fields to properly support 115 + normalised cache updates. 91 116 92 117 ## Fragment masking 93 118
+3 -1
packages/example-external-generator/package.json
··· 12 12 "dependencies": { 13 13 "@graphql-typed-document-node/core": "^3.2.0", 14 14 "@urql/core": "^3.0.0", 15 - "graphql": "^16.8.1" 15 + "graphql": "^16.8.1", 16 + "urql": "^4.0.6" 16 17 }, 17 18 "devDependencies": { 18 19 "@0no-co/graphqlsp": "file:../graphqlsp", 19 20 "@graphql-codegen/cli": "^5.0.0", 20 21 "@graphql-codegen/client-preset": "^4.1.0", 22 + "@types/react": "^18.2.45", 21 23 "ts-node": "^10.9.1", 22 24 "typescript": "^5.3.3" 23 25 }
-6
packages/example-external-generator/src/Pokemon.tsx
··· 14 14 } 15 15 `) 16 16 17 - export const WeakFields = graphql(` 18 - fragment weaknessFields on Pokemon { 19 - weaknesses 20 - } 21 - `) 22 - 23 17 export const Pokemon = (data: any) => { 24 18 const pokemon = useFragment(PokemonFields, data); 25 19 return `hi ${pokemon.name}`;
+3 -27
packages/example-external-generator/src/gql/gql.ts
··· 15 15 const documents = { 16 16 '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 17 types.PokemonFieldsFragmentDoc, 18 - '\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n': 19 - types.WeaknessFieldsFragmentDoc, 20 - '\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': 21 - types.PokDocument, 22 - '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n': 18 + '\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': 23 19 types.PoDocument, 24 - '\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n': 25 - types.PokemonsAreAwesomeDocument, 26 20 }; 27 21 28 22 /** ··· 49 43 * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 50 44 */ 51 45 export function graphql( 52 - source: '\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n' 53 - ): (typeof documents)['\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n']; 54 - /** 55 - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 56 - */ 57 - export function graphql( 58 - 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' 59 - ): (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']; 60 - /** 61 - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 62 - */ 63 - export function graphql( 64 - source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n' 65 - ): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n']; 66 - /** 67 - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 68 - */ 69 - export function graphql( 70 - source: '\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n' 71 - ): (typeof documents)['\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n']; 46 + 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' 47 + ): (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']; 72 48 73 49 export function graphql(source: string) { 74 50 return (documents as any)[source] ?? {};
+69 -162
packages/example-external-generator/src/gql/graphql.ts
··· 131 131 } | null; 132 132 } & { ' $fragmentName'?: 'PokemonFieldsFragment' }; 133 133 134 - export type WeaknessFieldsFragment = { 135 - __typename?: 'Pokemon'; 136 - weaknesses?: Array<PokemonType | null> | null; 137 - } & { ' $fragmentName'?: 'WeaknessFieldsFragment' }; 138 - 139 - export type PokQueryVariables = Exact<{ 140 - limit: Scalars['Int']['input']; 134 + export type PoQueryVariables = Exact<{ 135 + id: Scalars['ID']['input']; 141 136 }>; 142 137 143 - export type PokQuery = { 138 + export type PoQuery = { 144 139 __typename?: 'Query'; 145 - pokemons?: Array< 140 + pokemon?: 146 141 | ({ 147 142 __typename: 'Pokemon'; 148 143 id: string; 144 + fleeRate?: number | null; 149 145 name: string; 150 - fleeRate?: number | null; 151 - classification?: string | null; 146 + attacks?: { 147 + __typename?: 'AttacksConnection'; 148 + special?: Array<{ 149 + __typename?: 'Attack'; 150 + name?: string | null; 151 + damage?: number | null; 152 + } | null> | null; 153 + } | null; 154 + weight?: { 155 + __typename?: 'PokemonDimension'; 156 + minimum?: string | null; 157 + maximum?: string | null; 158 + } | null; 152 159 } & { 153 - ' $fragmentRefs'?: { 154 - PokemonFieldsFragment: PokemonFieldsFragment; 155 - WeaknessFieldsFragment: WeaknessFieldsFragment; 156 - }; 160 + ' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment }; 157 161 }) 158 - | null 159 - > | null; 160 - }; 161 - 162 - export type PoQueryVariables = Exact<{ 163 - id: Scalars['ID']['input']; 164 - }>; 165 - 166 - export type PoQuery = { 167 - __typename?: 'Query'; 168 - pokemon?: { 169 - __typename: 'Pokemon'; 170 - id: string; 171 - fleeRate?: number | null; 172 - } | null; 173 - }; 174 - 175 - export type PokemonsAreAwesomeQueryVariables = Exact<{ [key: string]: never }>; 176 - 177 - export type PokemonsAreAwesomeQuery = { 178 - __typename?: 'Query'; 179 - pokemons?: Array<{ __typename?: 'Pokemon'; id: string } | null> | null; 162 + | null; 180 163 }; 181 164 182 165 export const PokemonFieldsFragmentDoc = { ··· 222 205 }, 223 206 ], 224 207 } as unknown as DocumentNode<PokemonFieldsFragment, unknown>; 225 - export const WeaknessFieldsFragmentDoc = { 226 - kind: 'Document', 227 - definitions: [ 228 - { 229 - kind: 'FragmentDefinition', 230 - name: { kind: 'Name', value: 'weaknessFields' }, 231 - typeCondition: { 232 - kind: 'NamedType', 233 - name: { kind: 'Name', value: 'Pokemon' }, 234 - }, 235 - selectionSet: { 236 - kind: 'SelectionSet', 237 - selections: [ 238 - { kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } }, 239 - ], 240 - }, 241 - }, 242 - ], 243 - } as unknown as DocumentNode<WeaknessFieldsFragment, unknown>; 244 - export const PokDocument = { 208 + export const PoDocument = { 245 209 kind: 'Document', 246 210 definitions: [ 247 211 { 248 212 kind: 'OperationDefinition', 249 213 operation: 'query', 250 - name: { kind: 'Name', value: 'Pok' }, 214 + name: { kind: 'Name', value: 'Po' }, 251 215 variableDefinitions: [ 252 216 { 253 217 kind: 'VariableDefinition', 254 - variable: { 255 - kind: 'Variable', 256 - name: { kind: 'Name', value: 'limit' }, 257 - }, 218 + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, 258 219 type: { 259 220 kind: 'NonNullType', 260 - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 221 + type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, 261 222 }, 262 223 }, 263 224 ], ··· 266 227 selections: [ 267 228 { 268 229 kind: 'Field', 269 - name: { kind: 'Name', value: 'pokemons' }, 230 + name: { kind: 'Name', value: 'pokemon' }, 270 231 arguments: [ 271 232 { 272 233 kind: 'Argument', 273 - name: { kind: 'Name', value: 'limit' }, 234 + name: { kind: 'Name', value: 'id' }, 274 235 value: { 275 236 kind: 'Variable', 276 - name: { kind: 'Name', value: 'limit' }, 237 + name: { kind: 'Name', value: 'id' }, 277 238 }, 278 239 }, 279 240 ], ··· 281 242 kind: 'SelectionSet', 282 243 selections: [ 283 244 { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 284 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 285 245 { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 286 246 { 287 - kind: 'Field', 288 - name: { kind: 'Name', value: 'classification' }, 247 + kind: 'FragmentSpread', 248 + name: { kind: 'Name', value: 'pokemonFields' }, 289 249 }, 290 250 { 291 - kind: 'FragmentSpread', 292 - name: { kind: 'Name', value: 'pokemonFields' }, 251 + kind: 'Field', 252 + name: { kind: 'Name', value: 'attacks' }, 253 + selectionSet: { 254 + kind: 'SelectionSet', 255 + selections: [ 256 + { 257 + kind: 'Field', 258 + name: { kind: 'Name', value: 'special' }, 259 + selectionSet: { 260 + kind: 'SelectionSet', 261 + selections: [ 262 + { 263 + kind: 'Field', 264 + name: { kind: 'Name', value: 'name' }, 265 + }, 266 + { 267 + kind: 'Field', 268 + name: { kind: 'Name', value: 'damage' }, 269 + }, 270 + ], 271 + }, 272 + }, 273 + ], 274 + }, 293 275 }, 294 276 { 295 - kind: 'FragmentSpread', 296 - name: { kind: 'Name', value: 'weaknessFields' }, 277 + kind: 'Field', 278 + name: { kind: 'Name', value: 'weight' }, 279 + selectionSet: { 280 + kind: 'SelectionSet', 281 + selections: [ 282 + { 283 + kind: 'Field', 284 + name: { kind: 'Name', value: 'minimum' }, 285 + }, 286 + { 287 + kind: 'Field', 288 + name: { kind: 'Name', value: 'maximum' }, 289 + }, 290 + ], 291 + }, 297 292 }, 293 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 298 294 { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 299 295 ], 300 296 }, ··· 340 336 ], 341 337 }, 342 338 }, 343 - { 344 - kind: 'FragmentDefinition', 345 - name: { kind: 'Name', value: 'weaknessFields' }, 346 - typeCondition: { 347 - kind: 'NamedType', 348 - name: { kind: 'Name', value: 'Pokemon' }, 349 - }, 350 - selectionSet: { 351 - kind: 'SelectionSet', 352 - selections: [ 353 - { kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } }, 354 - ], 355 - }, 356 - }, 357 - ], 358 - } as unknown as DocumentNode<PokQuery, PokQueryVariables>; 359 - export const PoDocument = { 360 - kind: 'Document', 361 - definitions: [ 362 - { 363 - kind: 'OperationDefinition', 364 - operation: 'query', 365 - name: { kind: 'Name', value: 'Po' }, 366 - variableDefinitions: [ 367 - { 368 - kind: 'VariableDefinition', 369 - variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, 370 - type: { 371 - kind: 'NonNullType', 372 - type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, 373 - }, 374 - }, 375 - ], 376 - selectionSet: { 377 - kind: 'SelectionSet', 378 - selections: [ 379 - { 380 - kind: 'Field', 381 - name: { kind: 'Name', value: 'pokemon' }, 382 - arguments: [ 383 - { 384 - kind: 'Argument', 385 - name: { kind: 'Name', value: 'id' }, 386 - value: { 387 - kind: 'Variable', 388 - name: { kind: 'Name', value: 'id' }, 389 - }, 390 - }, 391 - ], 392 - selectionSet: { 393 - kind: 'SelectionSet', 394 - selections: [ 395 - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 396 - { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 397 - { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 398 - ], 399 - }, 400 - }, 401 - ], 402 - }, 403 - }, 404 339 ], 405 340 } as unknown as DocumentNode<PoQuery, PoQueryVariables>; 406 - export const PokemonsAreAwesomeDocument = { 407 - kind: 'Document', 408 - definitions: [ 409 - { 410 - kind: 'OperationDefinition', 411 - operation: 'query', 412 - name: { kind: 'Name', value: 'PokemonsAreAwesome' }, 413 - selectionSet: { 414 - kind: 'SelectionSet', 415 - selections: [ 416 - { 417 - kind: 'Field', 418 - name: { kind: 'Name', value: 'pokemons' }, 419 - selectionSet: { 420 - kind: 'SelectionSet', 421 - selections: [ 422 - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 423 - ], 424 - }, 425 - }, 426 - ], 427 - }, 428 - }, 429 - ], 430 - } as unknown as DocumentNode< 431 - PokemonsAreAwesomeQuery, 432 - PokemonsAreAwesomeQueryVariables 433 - >;
+37 -31
packages/example-external-generator/src/index.tsx
··· 1 - import { createClient } from '@urql/core'; 1 + import { createClient, useQuery } from 'urql'; 2 2 import { graphql } from './gql'; 3 - 4 - const x = graphql(` 5 - query Pok($limit: Int!) { 6 - pokemons(limit: $limit) @populate { 7 - id 8 - name 9 - fleeRate 10 - classification 11 - ...pokemonFields 12 - ...weaknessFields 13 - __typename 14 - } 15 - } 16 - `) 17 - 18 - const client = createClient({ 19 - url: '', 20 - }); 3 + import { Pokemon } from './Pokemon'; 21 4 22 5 const PokemonQuery = graphql(` 23 6 query Po($id: ID!) { 24 7 pokemon(id: $id) { 25 8 id 26 9 fleeRate 10 + ...pokemonFields 11 + attacks { 12 + special { 13 + name 14 + damage 15 + } 16 + } 17 + weight { 18 + minimum 19 + maximum 20 + } 21 + name 27 22 __typename 28 23 } 29 24 } 30 25 `); 31 26 32 - client 33 - .query(PokemonQuery, { id: '' }) 34 - .toPromise() 35 - .then(result => { 36 - result.data?.pokemon; 27 + const Pokemons = () => { 28 + const [result] = useQuery({ 29 + query: PokemonQuery, 30 + variables: { id: '' } 37 31 }); 32 + 33 + // Works 34 + console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 38 35 39 - const myQuery = graphql(` 40 - query PokemonsAreAwesome { 41 - pokemons { 42 - id 43 - } 44 - } 45 - `); 36 + // Works 37 + const { fleeRate } = result.data?.pokemon || {}; 38 + console.log(fleeRate) 39 + // Works 40 + const po = result.data?.pokemon; 41 + // @ts-expect-error 42 + const { pokemon: { weight: { minimum } } } = result.data || {}; 43 + console.log(po?.name, minimum) 44 + 45 + // Works 46 + const { pokemon } = result.data || {}; 47 + console.log(pokemon?.weight?.maximum) 48 + 49 + return <Pokemon data={result.data?.pokemon} />; 50 + } 51 +
+3 -1
packages/example-external-generator/tsconfig.json
··· 7 7 "disableTypegen": true, 8 8 "shouldCheckForColocatedFragments": false, 9 9 "template": "graphql", 10 - "templateIsCallExpression": true 10 + "templateIsCallExpression": true, 11 + "trackFieldUsage": true 11 12 } 12 13 ], 14 + "jsx": "react-jsx", 13 15 /* Language and Environment */ 14 16 "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 17 /* Modules */
+5 -212
packages/example/src/index.generated.ts
··· 1 1 import * as Types from '../__generated__/baseGraphQLSP'; 2 2 import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 - export type PokQueryVariables = Types.Exact<{ 4 - limit: Types.Scalars['Int']['input']; 3 + export type PoQueryVariables = Types.Exact<{ 4 + id: Types.Scalars['ID']['input']; 5 5 }>; 6 6 7 - export type PokQuery = { 7 + export type PoQuery = { 8 8 __typename: 'Query'; 9 - pokemons?: Array<{ 9 + pokemon?: { 10 10 __typename: 'Pokemon'; 11 11 id: string; 12 - name: string; 13 12 fleeRate?: number | null; 14 - classification?: string | null; 15 - weaknesses?: Array<Types.PokemonType | null> | null; 16 - attacks?: { 17 - __typename: 'AttacksConnection'; 18 - fast?: Array<{ 19 - __typename: 'Attack'; 20 - damage?: number | null; 21 - name?: string | null; 22 - } | null> | null; 23 - } | null; 24 - } | null> | null; 25 - }; 26 - 27 - export type PokemonFieldsFragment = { 28 - __typename: 'Pokemon'; 29 - id: string; 30 - name: string; 31 - attacks?: { 32 - __typename: 'AttacksConnection'; 33 - fast?: Array<{ 34 - __typename: 'Attack'; 35 - damage?: number | null; 36 - name?: string | null; 37 - } | null> | null; 38 13 } | null; 39 14 }; 40 15 41 - export type WeaknessFieldsFragment = { 42 - __typename: 'Pokemon'; 43 - weaknesses?: Array<Types.PokemonType | null> | null; 44 - }; 45 - 46 - export type PoQueryVariables = Types.Exact<{ 47 - id: Types.Scalars['ID']['input']; 48 - }>; 49 - 50 - export const PokemonFieldsFragmentDoc = { 51 - kind: 'Document', 52 - definitions: [ 53 - { 54 - kind: 'FragmentDefinition', 55 - name: { kind: 'Name', value: 'pokemonFields' }, 56 - typeCondition: { 57 - kind: 'NamedType', 58 - name: { kind: 'Name', value: 'Pokemon' }, 59 - }, 60 - selectionSet: { 61 - kind: 'SelectionSet', 62 - selections: [ 63 - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 64 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 65 - { 66 - kind: 'Field', 67 - name: { kind: 'Name', value: 'attacks' }, 68 - selectionSet: { 69 - kind: 'SelectionSet', 70 - selections: [ 71 - { 72 - kind: 'Field', 73 - name: { kind: 'Name', value: 'fast' }, 74 - selectionSet: { 75 - kind: 'SelectionSet', 76 - selections: [ 77 - { 78 - kind: 'Field', 79 - name: { kind: 'Name', value: 'damage' }, 80 - }, 81 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 82 - ], 83 - }, 84 - }, 85 - ], 86 - }, 87 - }, 88 - ], 89 - }, 90 - }, 91 - ], 92 - } as unknown as DocumentNode<PokemonFieldsFragment, unknown>; 93 - export const WeaknessFieldsFragmentDoc = { 94 - kind: 'Document', 95 - definitions: [ 96 - { 97 - kind: 'FragmentDefinition', 98 - name: { kind: 'Name', value: 'weaknessFields' }, 99 - typeCondition: { 100 - kind: 'NamedType', 101 - name: { kind: 'Name', value: 'Pokemon' }, 102 - }, 103 - selectionSet: { 104 - kind: 'SelectionSet', 105 - selections: [ 106 - { kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } }, 107 - ], 108 - }, 109 - }, 110 - ], 111 - } as unknown as DocumentNode<WeaknessFieldsFragment, unknown>; 112 - export const PokDocument = { 113 - kind: 'Document', 114 - definitions: [ 115 - { 116 - kind: 'OperationDefinition', 117 - operation: 'query', 118 - name: { kind: 'Name', value: 'Pok' }, 119 - variableDefinitions: [ 120 - { 121 - kind: 'VariableDefinition', 122 - variable: { 123 - kind: 'Variable', 124 - name: { kind: 'Name', value: 'limit' }, 125 - }, 126 - type: { 127 - kind: 'NonNullType', 128 - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 129 - }, 130 - }, 131 - ], 132 - selectionSet: { 133 - kind: 'SelectionSet', 134 - selections: [ 135 - { 136 - kind: 'Field', 137 - name: { kind: 'Name', value: 'pokemons' }, 138 - arguments: [ 139 - { 140 - kind: 'Argument', 141 - name: { kind: 'Name', value: 'limit' }, 142 - value: { 143 - kind: 'Variable', 144 - name: { kind: 'Name', value: 'limit' }, 145 - }, 146 - }, 147 - ], 148 - selectionSet: { 149 - kind: 'SelectionSet', 150 - selections: [ 151 - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 152 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 153 - { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 154 - { 155 - kind: 'FragmentSpread', 156 - name: { kind: 'Name', value: 'pokemonFields' }, 157 - }, 158 - { 159 - kind: 'FragmentSpread', 160 - name: { kind: 'Name', value: 'weaknessFields' }, 161 - }, 162 - { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 163 - ], 164 - }, 165 - }, 166 - ], 167 - }, 168 - }, 169 - { 170 - kind: 'FragmentDefinition', 171 - name: { kind: 'Name', value: 'pokemonFields' }, 172 - typeCondition: { 173 - kind: 'NamedType', 174 - name: { kind: 'Name', value: 'Pokemon' }, 175 - }, 176 - selectionSet: { 177 - kind: 'SelectionSet', 178 - selections: [ 179 - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 180 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 181 - { 182 - kind: 'Field', 183 - name: { kind: 'Name', value: 'attacks' }, 184 - selectionSet: { 185 - kind: 'SelectionSet', 186 - selections: [ 187 - { 188 - kind: 'Field', 189 - name: { kind: 'Name', value: 'fast' }, 190 - selectionSet: { 191 - kind: 'SelectionSet', 192 - selections: [ 193 - { 194 - kind: 'Field', 195 - name: { kind: 'Name', value: 'damage' }, 196 - }, 197 - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 198 - ], 199 - }, 200 - }, 201 - ], 202 - }, 203 - }, 204 - ], 205 - }, 206 - }, 207 - { 208 - kind: 'FragmentDefinition', 209 - name: { kind: 'Name', value: 'weaknessFields' }, 210 - typeCondition: { 211 - kind: 'NamedType', 212 - name: { kind: 'Name', value: 'Pokemon' }, 213 - }, 214 - selectionSet: { 215 - kind: 'SelectionSet', 216 - selections: [ 217 - { kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } }, 218 - ], 219 - }, 220 - }, 221 - ], 222 - } as unknown as DocumentNode<PokQuery, PokQueryVariables>; 223 16 export const PoDocument = { 224 17 kind: 'Document', 225 18 definitions: [ ··· 266 59 }, 267 60 }, 268 61 ], 269 - } as unknown as DocumentNode<PokQuery, PoQueryVariables>; 62 + } as unknown as DocumentNode<PoQuery, PoQueryVariables>;
-29
packages/example/src/index.ts
··· 1 1 import { gql, createClient } from '@urql/core'; 2 2 import { Pokemon, PokemonFields, WeakFields } from './Pokemon'; 3 3 4 - const x = gql` 5 - query Pok($limit: Int!) { 6 - pokemons(limit: $limit) @populate { 7 - id 8 - name 9 - fleeRate 10 - classification 11 - ...pokemonFields 12 - ...weaknessFields 13 - __typename 14 - } 15 - } 16 - 17 - ${PokemonFields} 18 - ${WeakFields} 19 - ` as typeof import('./index.generated').PokDocument; 20 - 21 - const client = createClient({ 22 - url: '', 23 - }); 24 - 25 4 const PokemonQuery = gql` 26 5 query Po($id: ID!) { 27 6 pokemon(id: $id) { ··· 38 17 .then(result => { 39 18 result.data?.pokemon; 40 19 }); 41 - 42 - const myQuery = gql` 43 - query PokemonsAreAwesome { 44 - pokemons { 45 - id 46 - } 47 - } 48 - `;
+25
packages/graphqlsp/README.md
··· 60 60 - `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions 61 61 - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 62 62 unused fragments and provide a message notifying you about them 63 + - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 64 + unused fields within the same file. 63 65 64 66 ### GraphQL Code Generator client-preset 65 67 ··· 74 76 "schema": "./schema.graphql", 75 77 "disableTypegen": true, 76 78 "templateIsCallExpression": true, 79 + "trackFieldUsage": true, 77 80 "template": "graphql" 78 81 } 79 82 ] 80 83 } 81 84 } 82 85 ``` 86 + 87 + ## Tracking unused fields 88 + 89 + Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost 90 + it will only track in the same file to encourage [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments). 91 + Secondly it supports a few patterns which we'll add to as time progresses: 92 + 93 + ```ts 94 + // Supported cases: 95 + const result = (await client.query()) || useFragment(); 96 + const [result] = useQuery(); // --> urql 97 + const { data } = useQuery(); // --> Apollo 98 + // Missing cases: 99 + const { field } = useFragment(); // can't destructure into your data from the start 100 + const [{ data }] = useQuery(); // can't follow array destructuring with object destructuring 101 + const { 102 + data: { pokemon }, 103 + } = useQuery(); // can't destructure into your data from the start 104 + ``` 105 + 106 + Lastly we don't track mutations/subscriptions as some folks will add additional fields to properly support 107 + normalised cache updates. 83 108 84 109 ## Fragment masking 85 110
+302 -2
packages/graphqlsp/src/diagnostics.ts
··· 7 7 OperationDefinitionNode, 8 8 parse, 9 9 print, 10 + visit, 10 11 } from 'graphql'; 11 12 import { LRUCache } from 'lru-cache'; 12 13 import fnv1a from '@sindresorhus/fnv1a'; ··· 15 16 findAllCallExpressions, 16 17 findAllImports, 17 18 findAllTaggedTemplateNodes, 19 + findNode, 18 20 getSource, 19 21 isFileDirty, 20 22 } from './ast'; 21 23 import { resolveTemplate } from './ast/resolve'; 22 24 import { generateTypedDocumentNodes } from './graphql/generateTypes'; 25 + import { Logger } from '.'; 23 26 24 27 const clientDirectives = new Set([ 25 28 'populate', ··· 40 43 export const MISSING_OPERATION_NAME_CODE = 52002; 41 44 export const MISSING_FRAGMENT_CODE = 52003; 42 45 export const USING_DEPRECATED_FIELD_CODE = 52004; 46 + export const UNUSED_FIELD_CODE = 52005; 43 47 44 48 let isGeneratingTypes = false; 45 49 ··· 93 97 let tsDiagnostics: ts.Diagnostic[] = []; 94 98 const cacheKey = fnv1a( 95 99 isCallExpression 96 - ? texts.join('-') + 100 + ? source.getText() + 97 101 fragments.map(x => print(x)).join('-') + 98 102 schema.version 99 103 : texts.join('-') + schema.version ··· 304 308 messageText: diag.message.split('\n')[0], 305 309 })); 306 310 307 - const importDiagnostics = checkImportsForFragments(source, info); 311 + const importDiagnostics = isCallExpression 312 + ? checkFieldUsageInFile( 313 + source, 314 + nodes as ts.NoSubstitutionTemplateLiteral[], 315 + info 316 + ) 317 + : checkImportsForFragments(source, info); 308 318 309 319 return [...tsDiagnostics, ...importDiagnostics]; 320 + }; 321 + 322 + const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => { 323 + let node: any = start; 324 + let counter = 0; 325 + while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) { 326 + node = node.parent; 327 + counter++; 328 + } 329 + return node; 330 + }; 331 + 332 + const traverseDestructuring = ( 333 + node: ts.ObjectBindingPattern, 334 + originalWip: Array<string>, 335 + allFields: Array<string>, 336 + source: ts.SourceFile, 337 + info: ts.server.PluginCreateInfo 338 + ): Array<string> => { 339 + const results = []; 340 + for (const binding of node.elements) { 341 + if (ts.isObjectBindingPattern(binding.name)) { 342 + const wip = [...originalWip]; 343 + if ( 344 + binding.propertyName && 345 + allFields.includes(binding.propertyName.getText()) && 346 + !originalWip.includes(binding.propertyName.getText()) 347 + ) { 348 + wip.push(binding.propertyName.getText()); 349 + } 350 + const traverseResult = traverseDestructuring( 351 + binding.name, 352 + wip, 353 + allFields, 354 + source, 355 + info 356 + ); 357 + 358 + results.push(...traverseResult); 359 + } else if (ts.isIdentifier(binding.name)) { 360 + const wip = [...originalWip]; 361 + if ( 362 + binding.propertyName && 363 + allFields.includes(binding.propertyName.getText()) && 364 + !originalWip.includes(binding.propertyName.getText()) 365 + ) { 366 + wip.push(binding.propertyName.getText()); 367 + } else { 368 + wip.push(binding.name.getText()); 369 + } 370 + 371 + const crawlResult = crawlScope( 372 + binding.name, 373 + wip, 374 + allFields, 375 + source, 376 + info 377 + ); 378 + 379 + results.push(...crawlResult); 380 + } 381 + } 382 + 383 + return results; 384 + }; 385 + 386 + const crawlScope = ( 387 + node: ts.Identifier | ts.BindingName, 388 + originalWip: Array<string>, 389 + allFields: Array<string>, 390 + source: ts.SourceFile, 391 + info: ts.server.PluginCreateInfo 392 + ): Array<string> => { 393 + let results: string[] = []; 394 + 395 + const references = info.languageService.getReferencesAtPosition( 396 + source.fileName, 397 + node.getStart() 398 + ); 399 + 400 + if (!references) return results; 401 + 402 + // Go over all the references tied to the result of 403 + // accessing our equery and collect them as fully 404 + // qualified paths (ideally ending in a leaf-node) 405 + results = references.flatMap(ref => { 406 + // If we get a reference to a different file we can bail 407 + if (ref.fileName !== source.fileName) return []; 408 + // We don't want to end back at our document so we narrow 409 + // the scope. 410 + if ( 411 + node.getStart() <= ref.textSpan.start && 412 + node.getEnd() >= ref.textSpan.start + ref.textSpan.length 413 + ) 414 + return []; 415 + 416 + let foundRef = findNode(source, ref.textSpan.start); 417 + if (!foundRef) return []; 418 + 419 + const pathParts = [...originalWip]; 420 + // In here we'll start crawling all the accessors of result 421 + // and try to determine the total path 422 + // - result.data.pokemon.name --> pokemon.name this is the easy route and never accesses 423 + // any of the recursive functions 424 + // - const pokemon = result.data.pokemon --> this initiates a new crawl with a renewed scope 425 + // - const { pokemon } = result.data --> this initiates a destructuring traversal which will 426 + // either end up in more destructuring traversals or a scope crawl 427 + while ( 428 + ts.isIdentifier(foundRef) || 429 + ts.isPropertyAccessExpression(foundRef) || 430 + ts.isElementAccessExpression(foundRef) || 431 + ts.isVariableDeclaration(foundRef) || 432 + ts.isBinaryExpression(foundRef) 433 + ) { 434 + if (ts.isVariableDeclaration(foundRef)) { 435 + if (ts.isIdentifier(foundRef.name)) { 436 + // We have already added the paths because of the right-hand expression, 437 + // const pokemon = result.data.pokemon --> we have pokemon as our path, 438 + // now re-crawling pokemon for all of its accessors should deliver us the usage 439 + // patterns... This might get expensive though if we need to perform this deeply. 440 + return crawlScope(foundRef.name, pathParts, allFields, source, info); 441 + } else if (ts.isObjectBindingPattern(foundRef.name)) { 442 + // First we need to traverse the left-hand side of the variable assignment, 443 + // this could be tree-like as we could be dealing with 444 + // - const { x: { y: z }, a: { b: { c, d }, e: { f } } } = result.data 445 + // Which we will need several paths for... 446 + // after doing that we need to re-crawl all of the resulting variables 447 + // Crawl down until we have either a leaf node or an object/array that can 448 + // be recrawled 449 + return traverseDestructuring( 450 + foundRef.name, 451 + pathParts, 452 + allFields, 453 + source, 454 + info 455 + ); 456 + } 457 + } else if ( 458 + ts.isIdentifier(foundRef) && 459 + allFields.includes(foundRef.text) && 460 + !pathParts.includes(foundRef.text) 461 + ) { 462 + pathParts.push(foundRef.text); 463 + } else if ( 464 + ts.isPropertyAccessExpression(foundRef) && 465 + allFields.includes(foundRef.name.text) && 466 + !pathParts.includes(foundRef.name.text) 467 + ) { 468 + pathParts.push(foundRef.name.text); 469 + } else if ( 470 + ts.isElementAccessExpression(foundRef) && 471 + ts.isStringLiteral(foundRef.argumentExpression) && 472 + allFields.includes(foundRef.argumentExpression.text) && 473 + !pathParts.includes(foundRef.argumentExpression.text) 474 + ) { 475 + pathParts.push(foundRef.argumentExpression.text); 476 + } 477 + 478 + foundRef = foundRef.parent; 479 + } 480 + 481 + return pathParts.join('.'); 482 + }); 483 + 484 + return results; 485 + }; 486 + 487 + const checkFieldUsageInFile = ( 488 + source: ts.SourceFile, 489 + nodes: ts.NoSubstitutionTemplateLiteral[], 490 + info: ts.server.PluginCreateInfo 491 + ) => { 492 + const logger: Logger = (msg: string) => 493 + info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 494 + const diagnostics: ts.Diagnostic[] = []; 495 + const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false; 496 + if (!shouldTrackFieldUsage) return diagnostics; 497 + 498 + nodes.forEach(node => { 499 + const nodeText = node.getText(); 500 + // Bailing for mutations/subscriptions as these could have small details 501 + // for normalised cache interactions 502 + if (nodeText.includes('mutation') || nodeText.includes('subscription')) 503 + return; 504 + 505 + const variableDeclaration = getVariableDeclaration(node); 506 + if (!ts.isVariableDeclaration(variableDeclaration)) return; 507 + 508 + const references = info.languageService.getReferencesAtPosition( 509 + source.fileName, 510 + variableDeclaration.name.getStart() 511 + ); 512 + if (!references) return; 513 + 514 + references.forEach(ref => { 515 + if (ref.fileName !== source.fileName) return; 516 + 517 + let found = findNode(source, ref.textSpan.start); 518 + while (found && !ts.isVariableStatement(found)) { 519 + found = found.parent; 520 + } 521 + 522 + if (!found || !ts.isVariableStatement(found)) return; 523 + 524 + const [output] = found.declarationList.declarations; 525 + 526 + if (output.name.getText() === variableDeclaration.name.getText()) return; 527 + 528 + const inProgress: string[] = []; 529 + const allPaths: string[] = []; 530 + const allFields: string[] = []; 531 + const reserved = ['id', '__typename']; 532 + const fieldToLoc = new Map<string, { start: number; length: number }>(); 533 + // This visitor gets all the leaf-paths in the document 534 + // as well as all fields that are part of the document 535 + // We need the leaf-paths to check usage and we need the 536 + // fields to validate whether an access on a given reference 537 + // is valid given the current document... 538 + visit(parse(node.getText().slice(1, -1)), { 539 + Field: { 540 + enter: node => { 541 + if (!reserved.includes(node.name.value)) { 542 + allFields.push(node.name.value); 543 + } 544 + 545 + if (!node.selectionSet && !reserved.includes(node.name.value)) { 546 + let p; 547 + if (inProgress.length) { 548 + p = inProgress.join('.') + '.' + node.name.value; 549 + } else { 550 + p = node.name.value; 551 + } 552 + allPaths.push(p); 553 + 554 + fieldToLoc.set(p, { 555 + start: node.name.loc!.start, 556 + length: node.name.loc!.end - node.name.loc!.start, 557 + }); 558 + } else if (node.selectionSet) { 559 + inProgress.push(node.name.value); 560 + } 561 + }, 562 + leave: node => { 563 + if (node.selectionSet) { 564 + inProgress.pop(); 565 + } 566 + }, 567 + }, 568 + }); 569 + 570 + let temp = output.name; 571 + // Supported cases: 572 + // - const result = await client.query() || useFragment() 573 + // - const [result] = useQuery() --> urql 574 + // - const { data } = useQuery() --> Apollo 575 + // - const { field } = useFragment() 576 + // - const [{ data }] = useQuery() 577 + // - const { data: { pokemon } } = useQuery() 578 + if ( 579 + ts.isArrayBindingPattern(temp) && 580 + ts.isBindingElement(temp.elements[0]) 581 + ) { 582 + temp = temp.elements[0].name; 583 + } 584 + 585 + let allAccess: string[] = []; 586 + if (ts.isObjectBindingPattern(temp)) { 587 + allAccess = traverseDestructuring(temp, [], allFields, source, info); 588 + } else { 589 + allAccess = crawlScope(temp, [], allFields, source, info); 590 + } 591 + 592 + const unused = allPaths.filter(x => !allAccess.includes(x)); 593 + unused.forEach(unusedField => { 594 + const loc = fieldToLoc.get(unusedField); 595 + if (!loc) return; 596 + 597 + diagnostics.push({ 598 + file: source, 599 + length: loc.length, 600 + start: node.getStart() + loc.start + 1, 601 + category: ts.DiagnosticCategory.Warning, 602 + code: UNUSED_FIELD_CODE, 603 + messageText: `Field '${unusedField}' is not used.`, 604 + }); 605 + }); 606 + }); 607 + }); 608 + 609 + return diagnostics; 310 610 }; 311 611 312 612 const checkImportsForFragments = (
+95 -9
pnpm-lock.yaml
··· 84 84 graphql: 85 85 specifier: ^16.8.1 86 86 version: 16.8.1 87 + urql: 88 + specifier: ^4.0.6 89 + version: 4.0.6(graphql@16.8.1)(react@18.2.0) 87 90 devDependencies: 88 91 '@0no-co/graphqlsp': 89 92 specifier: file:../graphqlsp ··· 94 97 '@graphql-codegen/client-preset': 95 98 specifier: ^4.1.0 96 99 version: 4.1.0(graphql@16.8.1) 100 + '@types/react': 101 + specifier: ^18.2.45 102 + version: 18.2.45 97 103 ts-node: 98 104 specifier: ^10.9.1 99 105 version: 10.9.1(@types/node@18.15.11)(typescript@5.3.3) ··· 179 185 specifier: ^5.3.3 180 186 version: 5.3.3 181 187 188 + test/e2e/fixture-project-unused-fields: 189 + dependencies: 190 + '@0no-co/graphqlsp': 191 + specifier: workspace:* 192 + version: link:../../../packages/graphqlsp 193 + '@graphql-typed-document-node/core': 194 + specifier: ^3.0.0 195 + version: 3.2.0(graphql@16.8.1) 196 + '@urql/core': 197 + specifier: ^4.0.4 198 + version: 4.2.2(graphql@16.8.1) 199 + graphql: 200 + specifier: ^16.0.0 201 + version: 16.8.1 202 + urql: 203 + specifier: ^4.0.4 204 + version: 4.0.6(graphql@16.8.1)(react@18.2.0) 205 + devDependencies: 206 + '@types/react': 207 + specifier: 18.2.45 208 + version: 18.2.45 209 + typescript: 210 + specifier: ^5.3.3 211 + version: 5.3.3 212 + 182 213 packages: 183 214 184 215 /@0no-co/graphql.web@1.0.0(graphql@16.8.1): 185 216 resolution: {integrity: sha512-JBq2pWyDchE1vVjj/+c4dzZ8stbpew4RrzpZ3vYdn1WJFGHfYg6YIX1fDdMKtSXJJM9FUlsoDOxemr9WMM2p+A==} 217 + peerDependencies: 218 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 219 + peerDependenciesMeta: 220 + graphql: 221 + optional: true 222 + dependencies: 223 + graphql: 16.8.1 224 + dev: false 225 + 226 + /@0no-co/graphql.web@1.0.4(graphql@16.8.1): 227 + resolution: {integrity: sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==} 186 228 peerDependencies: 187 229 graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 188 230 peerDependenciesMeta: ··· 2283 2325 resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} 2284 2326 dev: true 2285 2327 2328 + /@types/prop-types@15.7.11: 2329 + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} 2330 + dev: true 2331 + 2332 + /@types/react@18.2.45: 2333 + resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==} 2334 + dependencies: 2335 + '@types/prop-types': 15.7.11 2336 + '@types/scheduler': 0.16.8 2337 + csstype: 3.1.3 2338 + dev: true 2339 + 2340 + /@types/scheduler@0.16.8: 2341 + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} 2342 + dev: true 2343 + 2286 2344 /@types/semver@7.5.4: 2287 2345 resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} 2288 2346 dev: true ··· 2307 2365 dependencies: 2308 2366 '@0no-co/graphql.web': 1.0.0(graphql@16.8.1) 2309 2367 wonka: 6.3.1 2368 + transitivePeerDependencies: 2369 + - graphql 2370 + dev: false 2371 + 2372 + /@urql/core@4.2.2(graphql@16.8.1): 2373 + resolution: {integrity: sha512-TP1kheq9bnrEdnVbJqh0g0ZY/wfdpPeAzjiiDK+Tm+Pbi0O1Xdu6+fUJ/wJo5QpHZzkIyya4/AecG63e6scFqQ==} 2374 + dependencies: 2375 + '@0no-co/graphql.web': 1.0.4(graphql@16.8.1) 2376 + wonka: 6.3.4 2310 2377 transitivePeerDependencies: 2311 2378 - graphql 2312 2379 dev: false ··· 2991 3058 which: 2.0.2 2992 3059 dev: true 2993 3060 3061 + /csstype@3.1.3: 3062 + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 3063 + dev: true 3064 + 2994 3065 /csv-generate@3.4.3: 2995 3066 resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} 2996 3067 dev: true ··· 3431 3502 /fs.realpath@1.0.0: 3432 3503 resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 3433 3504 3434 - /fsevents@2.3.2: 3435 - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 3436 - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 3437 - os: [darwin] 3438 - requiresBuild: true 3439 - dev: true 3440 - optional: true 3441 - 3442 3505 /fsevents@2.3.3: 3443 3506 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 3444 3507 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} ··· 4804 4867 resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} 4805 4868 dev: true 4806 4869 4870 + /react@18.2.0: 4871 + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 4872 + engines: {node: '>=0.10.0'} 4873 + dependencies: 4874 + loose-envify: 1.4.0 4875 + dev: false 4876 + 4807 4877 /read-pkg-up@7.0.1: 4808 4878 resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} 4809 4879 engines: {node: '>=8'} ··· 4939 5009 engines: {node: '>=14.18.0', npm: '>=8.0.0'} 4940 5010 hasBin: true 4941 5011 optionalDependencies: 4942 - fsevents: 2.3.2 5012 + fsevents: 2.3.3 4943 5013 dev: true 4944 5014 4945 5015 /rollup@4.7.0: ··· 5572 5642 resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} 5573 5643 dev: true 5574 5644 5645 + /urql@4.0.6(graphql@16.8.1)(react@18.2.0): 5646 + resolution: {integrity: sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==} 5647 + peerDependencies: 5648 + react: '>= 16.8.0' 5649 + dependencies: 5650 + '@urql/core': 4.2.2(graphql@16.8.1) 5651 + react: 18.2.0 5652 + wonka: 6.3.4 5653 + transitivePeerDependencies: 5654 + - graphql 5655 + dev: false 5656 + 5575 5657 /util-deprecate@1.0.2: 5576 5658 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 5577 5659 dev: true ··· 5806 5888 5807 5889 /wonka@6.3.1: 5808 5890 resolution: {integrity: sha512-nJyGPcjuBiaLFn8QAlrHd+QjV9AlPO7snOWAhgx6aX0nQLMV6Wi0nqfrkmsXIH0efngbDOroOz2QyLnZMF16Hw==} 5891 + dev: false 5892 + 5893 + /wonka@6.3.4: 5894 + resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==} 5809 5895 dev: false 5810 5896 5811 5897 /wrap-ansi@6.2.0:
+3
test/e2e/fixture-project-unused-fields/.vscode/settings.json
··· 1 + { 2 + "typescript.tsdk": "node_modules/typescript/lib" 3 + }
+115
test/e2e/fixture-project-unused-fields/__generated__/baseGraphQLSP.ts
··· 1 + export type Maybe<T> = T | null; 2 + export type InputMaybe<T> = Maybe<T>; 3 + export type Exact<T extends { [key: string]: unknown }> = { 4 + [K in keyof T]: T[K]; 5 + }; 6 + export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 7 + [SubKey in K]?: Maybe<T[SubKey]>; 8 + }; 9 + export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 10 + [SubKey in K]: Maybe<T[SubKey]>; 11 + }; 12 + export type MakeEmpty< 13 + T extends { [key: string]: unknown }, 14 + K extends keyof T 15 + > = { [_ in K]?: never }; 16 + export type Incremental<T> = 17 + | T 18 + | { 19 + [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 20 + }; 21 + /** All built-in and custom scalars, mapped to their actual values */ 22 + export type Scalars = { 23 + ID: { input: string; output: string }; 24 + String: { input: string; output: string }; 25 + Boolean: { input: boolean; output: boolean }; 26 + Int: { input: number; output: number }; 27 + Float: { input: number; output: number }; 28 + }; 29 + 30 + /** Move a Pokémon can perform with the associated damage and type. */ 31 + export type Attack = { 32 + __typename: 'Attack'; 33 + damage?: Maybe<Scalars['Int']['output']>; 34 + name?: Maybe<Scalars['String']['output']>; 35 + type?: Maybe<PokemonType>; 36 + }; 37 + 38 + export type AttacksConnection = { 39 + __typename: 'AttacksConnection'; 40 + fast?: Maybe<Array<Maybe<Attack>>>; 41 + special?: Maybe<Array<Maybe<Attack>>>; 42 + }; 43 + 44 + /** Requirement that prevents an evolution through regular means of levelling up. */ 45 + export type EvolutionRequirement = { 46 + __typename: 'EvolutionRequirement'; 47 + amount?: Maybe<Scalars['Int']['output']>; 48 + name?: Maybe<Scalars['String']['output']>; 49 + }; 50 + 51 + export type Pokemon = { 52 + __typename: 'Pokemon'; 53 + attacks?: Maybe<AttacksConnection>; 54 + /** @deprecated And this is the reason why */ 55 + classification?: Maybe<Scalars['String']['output']>; 56 + evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 57 + evolutions?: Maybe<Array<Maybe<Pokemon>>>; 58 + /** Likelihood of an attempt to catch a Pokémon to fail. */ 59 + fleeRate?: Maybe<Scalars['Float']['output']>; 60 + height?: Maybe<PokemonDimension>; 61 + id: Scalars['ID']['output']; 62 + /** Maximum combat power a Pokémon may achieve at max level. */ 63 + maxCP?: Maybe<Scalars['Int']['output']>; 64 + /** Maximum health points a Pokémon may achieve at max level. */ 65 + maxHP?: Maybe<Scalars['Int']['output']>; 66 + name: Scalars['String']['output']; 67 + resistant?: Maybe<Array<Maybe<PokemonType>>>; 68 + types?: Maybe<Array<Maybe<PokemonType>>>; 69 + weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 70 + weight?: Maybe<PokemonDimension>; 71 + }; 72 + 73 + export type PokemonDimension = { 74 + __typename: 'PokemonDimension'; 75 + maximum?: Maybe<Scalars['String']['output']>; 76 + minimum?: Maybe<Scalars['String']['output']>; 77 + }; 78 + 79 + /** Elemental property associated with either a Pokémon or one of their moves. */ 80 + export type PokemonType = 81 + | 'Bug' 82 + | 'Dark' 83 + | 'Dragon' 84 + | 'Electric' 85 + | 'Fairy' 86 + | 'Fighting' 87 + | 'Fire' 88 + | 'Flying' 89 + | 'Ghost' 90 + | 'Grass' 91 + | 'Ground' 92 + | 'Ice' 93 + | 'Normal' 94 + | 'Poison' 95 + | 'Psychic' 96 + | 'Rock' 97 + | 'Steel' 98 + | 'Water'; 99 + 100 + export type Query = { 101 + __typename: 'Query'; 102 + /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 103 + pokemon?: Maybe<Pokemon>; 104 + /** List out all Pokémon, optionally in pages */ 105 + pokemons?: Maybe<Array<Maybe<Pokemon>>>; 106 + }; 107 + 108 + export type QueryPokemonArgs = { 109 + id: Scalars['ID']['input']; 110 + }; 111 + 112 + export type QueryPokemonsArgs = { 113 + limit?: InputMaybe<Scalars['Int']['input']>; 114 + skip?: InputMaybe<Scalars['Int']['input']>; 115 + };
+49
test/e2e/fixture-project-unused-fields/fixtures/destructuring.tsx
··· 1 + import { useQuery } from 'urql'; 2 + import { graphql } from './gql'; 3 + // @ts-expect-error 4 + import { Pokemon } from './fragment'; 5 + import * as React from 'react'; 6 + 7 + const PokemonQuery = graphql(` 8 + query Po($id: ID!) { 9 + pokemon(id: $id) { 10 + id 11 + fleeRate 12 + ...pokemonFields 13 + attacks { 14 + special { 15 + name 16 + damage 17 + } 18 + } 19 + weight { 20 + minimum 21 + maximum 22 + } 23 + name 24 + __typename 25 + } 26 + } 27 + `); 28 + 29 + const Pokemons = () => { 30 + const [result] = useQuery({ 31 + query: PokemonQuery, 32 + variables: { id: '' } 33 + }); 34 + 35 + // Works 36 + const { fleeRate } = result.data?.pokemon || {}; 37 + console.log(fleeRate) 38 + // @ts-expect-error 39 + const { pokemon: { weight: { minimum } } } = result.data || {}; 40 + console.log(minimum) 41 + 42 + // Works 43 + const { pokemon } = result.data || {}; 44 + console.log(pokemon?.weight?.maximum) 45 + 46 + // @ts-expect-error 47 + return <Pokemon data={result.data?.pokemon} />; 48 + } 49 +
+27
test/e2e/fixture-project-unused-fields/fixtures/fragment-destructuring.tsx
··· 1 + import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 + import { graphql } from './gql'; 3 + 4 + export const PokemonFields = graphql(` 5 + fragment pokemonFields on Pokemon { 6 + id 7 + name 8 + attacks { 9 + fast { 10 + damage 11 + name 12 + } 13 + } 14 + } 15 + `) 16 + 17 + export const Pokemon = (data: any) => { 18 + const { name } = useFragment(PokemonFields, data); 19 + return `hi ${name}`; 20 + }; 21 + 22 + export function useFragment<Type>( 23 + _fragment: TypedDocumentNode<Type>, 24 + data: any 25 + ): Type { 26 + return data; 27 + }
+27
test/e2e/fixture-project-unused-fields/fixtures/fragment.tsx
··· 1 + import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 + import { graphql } from './gql'; 3 + 4 + export const PokemonFields = graphql(` 5 + fragment pokemonFields on Pokemon { 6 + id 7 + name 8 + attacks { 9 + fast { 10 + damage 11 + name 12 + } 13 + } 14 + } 15 + `) 16 + 17 + export const Pokemon = (data: any) => { 18 + const pokemon = useFragment(PokemonFields, data); 19 + return `hi ${pokemon.name}`; 20 + }; 21 + 22 + export function useFragment<Type>( 23 + _fragment: TypedDocumentNode<Type>, 24 + data: any 25 + ): Type { 26 + return data; 27 + }
+85
test/e2e/fixture-project-unused-fields/fixtures/gql/fragment-masking.ts
··· 1 + import { 2 + ResultOf, 3 + DocumentTypeDecoration, 4 + TypedDocumentNode, 5 + } from '@graphql-typed-document-node/core'; 6 + import { FragmentDefinitionNode } from 'graphql'; 7 + import { Incremental } from './graphql'; 8 + 9 + export type FragmentType< 10 + TDocumentType extends DocumentTypeDecoration<any, any> 11 + > = TDocumentType extends DocumentTypeDecoration<infer TType, any> 12 + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 + ? TKey extends string 14 + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 + : never 16 + : never 17 + : never; 18 + 19 + // return non-nullable if `fragmentType` is non-nullable 20 + export function useFragment<TType>( 21 + _documentNode: DocumentTypeDecoration<TType, any>, 22 + fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> 23 + ): TType; 24 + // return nullable if `fragmentType` is nullable 25 + export function useFragment<TType>( 26 + _documentNode: DocumentTypeDecoration<TType, any>, 27 + fragmentType: 28 + | FragmentType<DocumentTypeDecoration<TType, any>> 29 + | null 30 + | undefined 31 + ): TType | null | undefined; 32 + // return array of non-nullable if `fragmentType` is array of non-nullable 33 + export function useFragment<TType>( 34 + _documentNode: DocumentTypeDecoration<TType, any>, 35 + fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 36 + ): ReadonlyArray<TType>; 37 + // return array of nullable if `fragmentType` is array of nullable 38 + export function useFragment<TType>( 39 + _documentNode: DocumentTypeDecoration<TType, any>, 40 + fragmentType: 41 + | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 42 + | null 43 + | undefined 44 + ): ReadonlyArray<TType> | null | undefined; 45 + export function useFragment<TType>( 46 + _documentNode: DocumentTypeDecoration<TType, any>, 47 + fragmentType: 48 + | FragmentType<DocumentTypeDecoration<TType, any>> 49 + | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 50 + | null 51 + | undefined 52 + ): TType | ReadonlyArray<TType> | null | undefined { 53 + return fragmentType as any; 54 + } 55 + 56 + export function makeFragmentData< 57 + F extends DocumentTypeDecoration<any, any>, 58 + FT extends ResultOf<F> 59 + >(data: FT, _fragment: F): FragmentType<F> { 60 + return data as FragmentType<F>; 61 + } 62 + export function isFragmentReady<TQuery, TFrag>( 63 + queryNode: DocumentTypeDecoration<TQuery, any>, 64 + fragmentNode: TypedDocumentNode<TFrag>, 65 + data: 66 + | FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> 67 + | null 68 + | undefined 69 + ): data is FragmentType<typeof fragmentNode> { 70 + const deferredFields = ( 71 + queryNode as { 72 + __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> }; 73 + } 74 + ).__meta__?.deferredFields; 75 + 76 + if (!deferredFields) return true; 77 + 78 + const fragDef = fragmentNode.definitions[0] as 79 + | FragmentDefinitionNode 80 + | undefined; 81 + const fragName = fragDef?.name?.value; 82 + 83 + const fields = (fragName && deferredFields[fragName]) || []; 84 + return fields.length > 0 && fields.every(field => data && field in data); 85 + }
+54
test/e2e/fixture-project-unused-fields/fixtures/gql/gql.ts
··· 1 + /* eslint-disable */ 2 + import * as types from './graphql'; 3 + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 + 5 + /** 6 + * Map of all GraphQL operations in the project. 7 + * 8 + * This map has several performance disadvantages: 9 + * 1. It is not tree-shakeable, so it will include all operations in the project. 10 + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 + * 3. It does not support dead code elimination, so it will add unused operations. 12 + * 13 + * Therefore it is highly recommended to use the babel or swc plugin for production. 14 + */ 15 + const documents = { 16 + '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 + types.PokemonFieldsFragmentDoc, 18 + '\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': 19 + types.PoDocument, 20 + }; 21 + 22 + /** 23 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 24 + * 25 + * 26 + * @example 27 + * ```ts 28 + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 29 + * ``` 30 + * 31 + * The query argument is unknown! 32 + * Please regenerate the types. 33 + */ 34 + export function graphql(source: string): unknown; 35 + 36 + /** 37 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 38 + */ 39 + export function graphql( 40 + source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 41 + ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 42 + /** 43 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 44 + */ 45 + export function graphql( 46 + 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' 47 + ): (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']; 48 + 49 + export function graphql(source: string) { 50 + return (documents as any)[source] ?? {}; 51 + } 52 + 53 + export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = 54 + TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
+340
test/e2e/fixture-project-unused-fields/fixtures/gql/graphql.ts
··· 1 + /* eslint-disable */ 2 + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 + export type Maybe<T> = T | null; 4 + export type InputMaybe<T> = Maybe<T>; 5 + export type Exact<T extends { [key: string]: unknown }> = { 6 + [K in keyof T]: T[K]; 7 + }; 8 + export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 9 + [SubKey in K]?: Maybe<T[SubKey]>; 10 + }; 11 + export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 12 + [SubKey in K]: Maybe<T[SubKey]>; 13 + }; 14 + export type MakeEmpty< 15 + T extends { [key: string]: unknown }, 16 + K extends keyof T 17 + > = { [_ in K]?: never }; 18 + export type Incremental<T> = 19 + | T 20 + | { 21 + [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 22 + }; 23 + /** All built-in and custom scalars, mapped to their actual values */ 24 + export type Scalars = { 25 + ID: { input: string; output: string }; 26 + String: { input: string; output: string }; 27 + Boolean: { input: boolean; output: boolean }; 28 + Int: { input: number; output: number }; 29 + Float: { input: number; output: number }; 30 + }; 31 + 32 + /** Move a Pokémon can perform with the associated damage and type. */ 33 + export type Attack = { 34 + __typename?: 'Attack'; 35 + damage?: Maybe<Scalars['Int']['output']>; 36 + name?: Maybe<Scalars['String']['output']>; 37 + type?: Maybe<PokemonType>; 38 + }; 39 + 40 + export type AttacksConnection = { 41 + __typename?: 'AttacksConnection'; 42 + fast?: Maybe<Array<Maybe<Attack>>>; 43 + special?: Maybe<Array<Maybe<Attack>>>; 44 + }; 45 + 46 + /** Requirement that prevents an evolution through regular means of levelling up. */ 47 + export type EvolutionRequirement = { 48 + __typename?: 'EvolutionRequirement'; 49 + amount?: Maybe<Scalars['Int']['output']>; 50 + name?: Maybe<Scalars['String']['output']>; 51 + }; 52 + 53 + export type Pokemon = { 54 + __typename?: 'Pokemon'; 55 + attacks?: Maybe<AttacksConnection>; 56 + /** @deprecated And this is the reason why */ 57 + classification?: Maybe<Scalars['String']['output']>; 58 + evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 59 + evolutions?: Maybe<Array<Maybe<Pokemon>>>; 60 + /** Likelihood of an attempt to catch a Pokémon to fail. */ 61 + fleeRate?: Maybe<Scalars['Float']['output']>; 62 + height?: Maybe<PokemonDimension>; 63 + id: Scalars['ID']['output']; 64 + /** Maximum combat power a Pokémon may achieve at max level. */ 65 + maxCP?: Maybe<Scalars['Int']['output']>; 66 + /** Maximum health points a Pokémon may achieve at max level. */ 67 + maxHP?: Maybe<Scalars['Int']['output']>; 68 + name: Scalars['String']['output']; 69 + resistant?: Maybe<Array<Maybe<PokemonType>>>; 70 + types?: Maybe<Array<Maybe<PokemonType>>>; 71 + weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 72 + weight?: Maybe<PokemonDimension>; 73 + }; 74 + 75 + export type PokemonDimension = { 76 + __typename?: 'PokemonDimension'; 77 + maximum?: Maybe<Scalars['String']['output']>; 78 + minimum?: Maybe<Scalars['String']['output']>; 79 + }; 80 + 81 + /** Elemental property associated with either a Pokémon or one of their moves. */ 82 + export enum PokemonType { 83 + Bug = 'Bug', 84 + Dark = 'Dark', 85 + Dragon = 'Dragon', 86 + Electric = 'Electric', 87 + Fairy = 'Fairy', 88 + Fighting = 'Fighting', 89 + Fire = 'Fire', 90 + Flying = 'Flying', 91 + Ghost = 'Ghost', 92 + Grass = 'Grass', 93 + Ground = 'Ground', 94 + Ice = 'Ice', 95 + Normal = 'Normal', 96 + Poison = 'Poison', 97 + Psychic = 'Psychic', 98 + Rock = 'Rock', 99 + Steel = 'Steel', 100 + Water = 'Water', 101 + } 102 + 103 + export type Query = { 104 + __typename?: 'Query'; 105 + /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 106 + pokemon?: Maybe<Pokemon>; 107 + /** List out all Pokémon, optionally in pages */ 108 + pokemons?: Maybe<Array<Maybe<Pokemon>>>; 109 + }; 110 + 111 + export type QueryPokemonArgs = { 112 + id: Scalars['ID']['input']; 113 + }; 114 + 115 + export type QueryPokemonsArgs = { 116 + limit?: InputMaybe<Scalars['Int']['input']>; 117 + skip?: InputMaybe<Scalars['Int']['input']>; 118 + }; 119 + 120 + export type PokemonFieldsFragment = { 121 + __typename?: 'Pokemon'; 122 + id: string; 123 + name: string; 124 + attacks?: { 125 + __typename?: 'AttacksConnection'; 126 + fast?: Array<{ 127 + __typename?: 'Attack'; 128 + damage?: number | null; 129 + name?: string | null; 130 + } | null> | null; 131 + } | null; 132 + } & { ' $fragmentName'?: 'PokemonFieldsFragment' }; 133 + 134 + export type PoQueryVariables = Exact<{ 135 + id: Scalars['ID']['input']; 136 + }>; 137 + 138 + export type PoQuery = { 139 + __typename?: 'Query'; 140 + pokemon?: 141 + | ({ 142 + __typename: 'Pokemon'; 143 + id: string; 144 + fleeRate?: number | null; 145 + name: string; 146 + attacks?: { 147 + __typename?: 'AttacksConnection'; 148 + special?: Array<{ 149 + __typename?: 'Attack'; 150 + name?: string | null; 151 + damage?: number | null; 152 + } | null> | null; 153 + } | null; 154 + weight?: { 155 + __typename?: 'PokemonDimension'; 156 + minimum?: string | null; 157 + maximum?: string | null; 158 + } | null; 159 + } & { 160 + ' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment }; 161 + }) 162 + | null; 163 + }; 164 + 165 + export const PokemonFieldsFragmentDoc = { 166 + kind: 'Document', 167 + definitions: [ 168 + { 169 + kind: 'FragmentDefinition', 170 + name: { kind: 'Name', value: 'pokemonFields' }, 171 + typeCondition: { 172 + kind: 'NamedType', 173 + name: { kind: 'Name', value: 'Pokemon' }, 174 + }, 175 + selectionSet: { 176 + kind: 'SelectionSet', 177 + selections: [ 178 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 179 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 180 + { 181 + kind: 'Field', 182 + name: { kind: 'Name', value: 'attacks' }, 183 + selectionSet: { 184 + kind: 'SelectionSet', 185 + selections: [ 186 + { 187 + kind: 'Field', 188 + name: { kind: 'Name', value: 'fast' }, 189 + selectionSet: { 190 + kind: 'SelectionSet', 191 + selections: [ 192 + { 193 + kind: 'Field', 194 + name: { kind: 'Name', value: 'damage' }, 195 + }, 196 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 197 + ], 198 + }, 199 + }, 200 + ], 201 + }, 202 + }, 203 + ], 204 + }, 205 + }, 206 + ], 207 + } as unknown as DocumentNode<PokemonFieldsFragment, unknown>; 208 + export const PoDocument = { 209 + kind: 'Document', 210 + definitions: [ 211 + { 212 + kind: 'OperationDefinition', 213 + operation: 'query', 214 + name: { kind: 'Name', value: 'Po' }, 215 + variableDefinitions: [ 216 + { 217 + kind: 'VariableDefinition', 218 + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, 219 + type: { 220 + kind: 'NonNullType', 221 + type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, 222 + }, 223 + }, 224 + ], 225 + selectionSet: { 226 + kind: 'SelectionSet', 227 + selections: [ 228 + { 229 + kind: 'Field', 230 + name: { kind: 'Name', value: 'pokemon' }, 231 + arguments: [ 232 + { 233 + kind: 'Argument', 234 + name: { kind: 'Name', value: 'id' }, 235 + value: { 236 + kind: 'Variable', 237 + name: { kind: 'Name', value: 'id' }, 238 + }, 239 + }, 240 + ], 241 + selectionSet: { 242 + kind: 'SelectionSet', 243 + selections: [ 244 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 245 + { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 246 + { 247 + kind: 'FragmentSpread', 248 + name: { kind: 'Name', value: 'pokemonFields' }, 249 + }, 250 + { 251 + kind: 'Field', 252 + name: { kind: 'Name', value: 'attacks' }, 253 + selectionSet: { 254 + kind: 'SelectionSet', 255 + selections: [ 256 + { 257 + kind: 'Field', 258 + name: { kind: 'Name', value: 'special' }, 259 + selectionSet: { 260 + kind: 'SelectionSet', 261 + selections: [ 262 + { 263 + kind: 'Field', 264 + name: { kind: 'Name', value: 'name' }, 265 + }, 266 + { 267 + kind: 'Field', 268 + name: { kind: 'Name', value: 'damage' }, 269 + }, 270 + ], 271 + }, 272 + }, 273 + ], 274 + }, 275 + }, 276 + { 277 + kind: 'Field', 278 + name: { kind: 'Name', value: 'weight' }, 279 + selectionSet: { 280 + kind: 'SelectionSet', 281 + selections: [ 282 + { 283 + kind: 'Field', 284 + name: { kind: 'Name', value: 'minimum' }, 285 + }, 286 + { 287 + kind: 'Field', 288 + name: { kind: 'Name', value: 'maximum' }, 289 + }, 290 + ], 291 + }, 292 + }, 293 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 294 + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 295 + ], 296 + }, 297 + }, 298 + ], 299 + }, 300 + }, 301 + { 302 + kind: 'FragmentDefinition', 303 + name: { kind: 'Name', value: 'pokemonFields' }, 304 + typeCondition: { 305 + kind: 'NamedType', 306 + name: { kind: 'Name', value: 'Pokemon' }, 307 + }, 308 + selectionSet: { 309 + kind: 'SelectionSet', 310 + selections: [ 311 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 312 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 313 + { 314 + kind: 'Field', 315 + name: { kind: 'Name', value: 'attacks' }, 316 + selectionSet: { 317 + kind: 'SelectionSet', 318 + selections: [ 319 + { 320 + kind: 'Field', 321 + name: { kind: 'Name', value: 'fast' }, 322 + selectionSet: { 323 + kind: 'SelectionSet', 324 + selections: [ 325 + { 326 + kind: 'Field', 327 + name: { kind: 'Name', value: 'damage' }, 328 + }, 329 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 330 + ], 331 + }, 332 + }, 333 + ], 334 + }, 335 + }, 336 + ], 337 + }, 338 + }, 339 + ], 340 + } as unknown as DocumentNode<PoQuery, PoQueryVariables>;
+2
test/e2e/fixture-project-unused-fields/fixtures/gql/index.ts
··· 1 + export * from './fragment-masking'; 2 + export * from './gql';
+39
test/e2e/fixture-project-unused-fields/fixtures/immediate-destructuring.tsx
··· 1 + import { useQuery } from 'urql'; 2 + import { graphql } from './gql'; 3 + // @ts-expect-error 4 + import { Pokemon } from './fragment'; 5 + import * as React from 'react'; 6 + 7 + const PokemonQuery = graphql(` 8 + query Po($id: ID!) { 9 + pokemon(id: $id) { 10 + id 11 + fleeRate 12 + ...pokemonFields 13 + attacks { 14 + special { 15 + name 16 + damage 17 + } 18 + } 19 + weight { 20 + minimum 21 + maximum 22 + } 23 + name 24 + __typename 25 + } 26 + } 27 + `); 28 + 29 + const Pokemons = () => { 30 + // @ts-expect-error 31 + const [{ data: { pokemon: { fleeRate, weight: { minimum, maximum } } } }] = useQuery({ 32 + query: PokemonQuery, 33 + variables: { id: '' } 34 + }); 35 + 36 + // @ts-expect-error 37 + return <Pokemon data={{ fleeRate, weight: { minimum, maximum } }} />; 38 + } 39 +
+42
test/e2e/fixture-project-unused-fields/fixtures/property-access.tsx
··· 1 + import { useQuery } from 'urql'; 2 + import { graphql } from './gql'; 3 + // @ts-expect-error 4 + import { Pokemon } from './fragment'; 5 + import * as React from 'react'; 6 + 7 + const PokemonQuery = graphql(` 8 + query Po($id: ID!) { 9 + pokemon(id: $id) { 10 + id 11 + fleeRate 12 + ...pokemonFields 13 + attacks { 14 + special { 15 + name 16 + damage 17 + } 18 + } 19 + weight { 20 + minimum 21 + maximum 22 + } 23 + name 24 + __typename 25 + } 26 + } 27 + `); 28 + 29 + const Pokemons = () => { 30 + const [result] = useQuery({ 31 + query: PokemonQuery, 32 + variables: { id: '' } 33 + }); 34 + 35 + const pokemon = result.data?.pokemon 36 + console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 37 + console.log(pokemon?.name) 38 + 39 + // @ts-expect-error 40 + return <Pokemon data={result.data?.pokemon} />; 41 + } 42 +
+85
test/e2e/fixture-project-unused-fields/gql/fragment-masking.ts
··· 1 + import { 2 + ResultOf, 3 + DocumentTypeDecoration, 4 + TypedDocumentNode, 5 + } from '@graphql-typed-document-node/core'; 6 + import { FragmentDefinitionNode } from 'graphql'; 7 + import { Incremental } from './graphql'; 8 + 9 + export type FragmentType< 10 + TDocumentType extends DocumentTypeDecoration<any, any> 11 + > = TDocumentType extends DocumentTypeDecoration<infer TType, any> 12 + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 + ? TKey extends string 14 + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 + : never 16 + : never 17 + : never; 18 + 19 + // return non-nullable if `fragmentType` is non-nullable 20 + export function useFragment<TType>( 21 + _documentNode: DocumentTypeDecoration<TType, any>, 22 + fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> 23 + ): TType; 24 + // return nullable if `fragmentType` is nullable 25 + export function useFragment<TType>( 26 + _documentNode: DocumentTypeDecoration<TType, any>, 27 + fragmentType: 28 + | FragmentType<DocumentTypeDecoration<TType, any>> 29 + | null 30 + | undefined 31 + ): TType | null | undefined; 32 + // return array of non-nullable if `fragmentType` is array of non-nullable 33 + export function useFragment<TType>( 34 + _documentNode: DocumentTypeDecoration<TType, any>, 35 + fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 36 + ): ReadonlyArray<TType>; 37 + // return array of nullable if `fragmentType` is array of nullable 38 + export function useFragment<TType>( 39 + _documentNode: DocumentTypeDecoration<TType, any>, 40 + fragmentType: 41 + | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 42 + | null 43 + | undefined 44 + ): ReadonlyArray<TType> | null | undefined; 45 + export function useFragment<TType>( 46 + _documentNode: DocumentTypeDecoration<TType, any>, 47 + fragmentType: 48 + | FragmentType<DocumentTypeDecoration<TType, any>> 49 + | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> 50 + | null 51 + | undefined 52 + ): TType | ReadonlyArray<TType> | null | undefined { 53 + return fragmentType as any; 54 + } 55 + 56 + export function makeFragmentData< 57 + F extends DocumentTypeDecoration<any, any>, 58 + FT extends ResultOf<F> 59 + >(data: FT, _fragment: F): FragmentType<F> { 60 + return data as FragmentType<F>; 61 + } 62 + export function isFragmentReady<TQuery, TFrag>( 63 + queryNode: DocumentTypeDecoration<TQuery, any>, 64 + fragmentNode: TypedDocumentNode<TFrag>, 65 + data: 66 + | FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> 67 + | null 68 + | undefined 69 + ): data is FragmentType<typeof fragmentNode> { 70 + const deferredFields = ( 71 + queryNode as { 72 + __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> }; 73 + } 74 + ).__meta__?.deferredFields; 75 + 76 + if (!deferredFields) return true; 77 + 78 + const fragDef = fragmentNode.definitions[0] as 79 + | FragmentDefinitionNode 80 + | undefined; 81 + const fragName = fragDef?.name?.value; 82 + 83 + const fields = (fragName && deferredFields[fragName]) || []; 84 + return fields.length > 0 && fields.every(field => data && field in data); 85 + }
+54
test/e2e/fixture-project-unused-fields/gql/gql.ts
··· 1 + /* eslint-disable */ 2 + import * as types from './graphql'; 3 + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 + 5 + /** 6 + * Map of all GraphQL operations in the project. 7 + * 8 + * This map has several performance disadvantages: 9 + * 1. It is not tree-shakeable, so it will include all operations in the project. 10 + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 + * 3. It does not support dead code elimination, so it will add unused operations. 12 + * 13 + * Therefore it is highly recommended to use the babel or swc plugin for production. 14 + */ 15 + const documents = { 16 + '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 + types.PokemonFieldsFragmentDoc, 18 + '\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': 19 + types.PoDocument, 20 + }; 21 + 22 + /** 23 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 24 + * 25 + * 26 + * @example 27 + * ```ts 28 + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 29 + * ``` 30 + * 31 + * The query argument is unknown! 32 + * Please regenerate the types. 33 + */ 34 + export function graphql(source: string): unknown; 35 + 36 + /** 37 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 38 + */ 39 + export function graphql( 40 + source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 41 + ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 42 + /** 43 + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 44 + */ 45 + export function graphql( 46 + 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' 47 + ): (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']; 48 + 49 + export function graphql(source: string) { 50 + return (documents as any)[source] ?? {}; 51 + } 52 + 53 + export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = 54 + TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
+340
test/e2e/fixture-project-unused-fields/gql/graphql.ts
··· 1 + /* eslint-disable */ 2 + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 + export type Maybe<T> = T | null; 4 + export type InputMaybe<T> = Maybe<T>; 5 + export type Exact<T extends { [key: string]: unknown }> = { 6 + [K in keyof T]: T[K]; 7 + }; 8 + export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 9 + [SubKey in K]?: Maybe<T[SubKey]>; 10 + }; 11 + export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 12 + [SubKey in K]: Maybe<T[SubKey]>; 13 + }; 14 + export type MakeEmpty< 15 + T extends { [key: string]: unknown }, 16 + K extends keyof T 17 + > = { [_ in K]?: never }; 18 + export type Incremental<T> = 19 + | T 20 + | { 21 + [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 22 + }; 23 + /** All built-in and custom scalars, mapped to their actual values */ 24 + export type Scalars = { 25 + ID: { input: string; output: string }; 26 + String: { input: string; output: string }; 27 + Boolean: { input: boolean; output: boolean }; 28 + Int: { input: number; output: number }; 29 + Float: { input: number; output: number }; 30 + }; 31 + 32 + /** Move a Pokémon can perform with the associated damage and type. */ 33 + export type Attack = { 34 + __typename?: 'Attack'; 35 + damage?: Maybe<Scalars['Int']['output']>; 36 + name?: Maybe<Scalars['String']['output']>; 37 + type?: Maybe<PokemonType>; 38 + }; 39 + 40 + export type AttacksConnection = { 41 + __typename?: 'AttacksConnection'; 42 + fast?: Maybe<Array<Maybe<Attack>>>; 43 + special?: Maybe<Array<Maybe<Attack>>>; 44 + }; 45 + 46 + /** Requirement that prevents an evolution through regular means of levelling up. */ 47 + export type EvolutionRequirement = { 48 + __typename?: 'EvolutionRequirement'; 49 + amount?: Maybe<Scalars['Int']['output']>; 50 + name?: Maybe<Scalars['String']['output']>; 51 + }; 52 + 53 + export type Pokemon = { 54 + __typename?: 'Pokemon'; 55 + attacks?: Maybe<AttacksConnection>; 56 + /** @deprecated And this is the reason why */ 57 + classification?: Maybe<Scalars['String']['output']>; 58 + evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 59 + evolutions?: Maybe<Array<Maybe<Pokemon>>>; 60 + /** Likelihood of an attempt to catch a Pokémon to fail. */ 61 + fleeRate?: Maybe<Scalars['Float']['output']>; 62 + height?: Maybe<PokemonDimension>; 63 + id: Scalars['ID']['output']; 64 + /** Maximum combat power a Pokémon may achieve at max level. */ 65 + maxCP?: Maybe<Scalars['Int']['output']>; 66 + /** Maximum health points a Pokémon may achieve at max level. */ 67 + maxHP?: Maybe<Scalars['Int']['output']>; 68 + name: Scalars['String']['output']; 69 + resistant?: Maybe<Array<Maybe<PokemonType>>>; 70 + types?: Maybe<Array<Maybe<PokemonType>>>; 71 + weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 72 + weight?: Maybe<PokemonDimension>; 73 + }; 74 + 75 + export type PokemonDimension = { 76 + __typename?: 'PokemonDimension'; 77 + maximum?: Maybe<Scalars['String']['output']>; 78 + minimum?: Maybe<Scalars['String']['output']>; 79 + }; 80 + 81 + /** Elemental property associated with either a Pokémon or one of their moves. */ 82 + export enum PokemonType { 83 + Bug = 'Bug', 84 + Dark = 'Dark', 85 + Dragon = 'Dragon', 86 + Electric = 'Electric', 87 + Fairy = 'Fairy', 88 + Fighting = 'Fighting', 89 + Fire = 'Fire', 90 + Flying = 'Flying', 91 + Ghost = 'Ghost', 92 + Grass = 'Grass', 93 + Ground = 'Ground', 94 + Ice = 'Ice', 95 + Normal = 'Normal', 96 + Poison = 'Poison', 97 + Psychic = 'Psychic', 98 + Rock = 'Rock', 99 + Steel = 'Steel', 100 + Water = 'Water', 101 + } 102 + 103 + export type Query = { 104 + __typename?: 'Query'; 105 + /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 106 + pokemon?: Maybe<Pokemon>; 107 + /** List out all Pokémon, optionally in pages */ 108 + pokemons?: Maybe<Array<Maybe<Pokemon>>>; 109 + }; 110 + 111 + export type QueryPokemonArgs = { 112 + id: Scalars['ID']['input']; 113 + }; 114 + 115 + export type QueryPokemonsArgs = { 116 + limit?: InputMaybe<Scalars['Int']['input']>; 117 + skip?: InputMaybe<Scalars['Int']['input']>; 118 + }; 119 + 120 + export type PokemonFieldsFragment = { 121 + __typename?: 'Pokemon'; 122 + id: string; 123 + name: string; 124 + attacks?: { 125 + __typename?: 'AttacksConnection'; 126 + fast?: Array<{ 127 + __typename?: 'Attack'; 128 + damage?: number | null; 129 + name?: string | null; 130 + } | null> | null; 131 + } | null; 132 + } & { ' $fragmentName'?: 'PokemonFieldsFragment' }; 133 + 134 + export type PoQueryVariables = Exact<{ 135 + id: Scalars['ID']['input']; 136 + }>; 137 + 138 + export type PoQuery = { 139 + __typename?: 'Query'; 140 + pokemon?: 141 + | ({ 142 + __typename: 'Pokemon'; 143 + id: string; 144 + fleeRate?: number | null; 145 + name: string; 146 + attacks?: { 147 + __typename?: 'AttacksConnection'; 148 + special?: Array<{ 149 + __typename?: 'Attack'; 150 + name?: string | null; 151 + damage?: number | null; 152 + } | null> | null; 153 + } | null; 154 + weight?: { 155 + __typename?: 'PokemonDimension'; 156 + minimum?: string | null; 157 + maximum?: string | null; 158 + } | null; 159 + } & { 160 + ' $fragmentRefs'?: { PokemonFieldsFragment: PokemonFieldsFragment }; 161 + }) 162 + | null; 163 + }; 164 + 165 + export const PokemonFieldsFragmentDoc = { 166 + kind: 'Document', 167 + definitions: [ 168 + { 169 + kind: 'FragmentDefinition', 170 + name: { kind: 'Name', value: 'pokemonFields' }, 171 + typeCondition: { 172 + kind: 'NamedType', 173 + name: { kind: 'Name', value: 'Pokemon' }, 174 + }, 175 + selectionSet: { 176 + kind: 'SelectionSet', 177 + selections: [ 178 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 179 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 180 + { 181 + kind: 'Field', 182 + name: { kind: 'Name', value: 'attacks' }, 183 + selectionSet: { 184 + kind: 'SelectionSet', 185 + selections: [ 186 + { 187 + kind: 'Field', 188 + name: { kind: 'Name', value: 'fast' }, 189 + selectionSet: { 190 + kind: 'SelectionSet', 191 + selections: [ 192 + { 193 + kind: 'Field', 194 + name: { kind: 'Name', value: 'damage' }, 195 + }, 196 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 197 + ], 198 + }, 199 + }, 200 + ], 201 + }, 202 + }, 203 + ], 204 + }, 205 + }, 206 + ], 207 + } as unknown as DocumentNode<PokemonFieldsFragment, unknown>; 208 + export const PoDocument = { 209 + kind: 'Document', 210 + definitions: [ 211 + { 212 + kind: 'OperationDefinition', 213 + operation: 'query', 214 + name: { kind: 'Name', value: 'Po' }, 215 + variableDefinitions: [ 216 + { 217 + kind: 'VariableDefinition', 218 + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, 219 + type: { 220 + kind: 'NonNullType', 221 + type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, 222 + }, 223 + }, 224 + ], 225 + selectionSet: { 226 + kind: 'SelectionSet', 227 + selections: [ 228 + { 229 + kind: 'Field', 230 + name: { kind: 'Name', value: 'pokemon' }, 231 + arguments: [ 232 + { 233 + kind: 'Argument', 234 + name: { kind: 'Name', value: 'id' }, 235 + value: { 236 + kind: 'Variable', 237 + name: { kind: 'Name', value: 'id' }, 238 + }, 239 + }, 240 + ], 241 + selectionSet: { 242 + kind: 'SelectionSet', 243 + selections: [ 244 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 245 + { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 246 + { 247 + kind: 'FragmentSpread', 248 + name: { kind: 'Name', value: 'pokemonFields' }, 249 + }, 250 + { 251 + kind: 'Field', 252 + name: { kind: 'Name', value: 'attacks' }, 253 + selectionSet: { 254 + kind: 'SelectionSet', 255 + selections: [ 256 + { 257 + kind: 'Field', 258 + name: { kind: 'Name', value: 'special' }, 259 + selectionSet: { 260 + kind: 'SelectionSet', 261 + selections: [ 262 + { 263 + kind: 'Field', 264 + name: { kind: 'Name', value: 'name' }, 265 + }, 266 + { 267 + kind: 'Field', 268 + name: { kind: 'Name', value: 'damage' }, 269 + }, 270 + ], 271 + }, 272 + }, 273 + ], 274 + }, 275 + }, 276 + { 277 + kind: 'Field', 278 + name: { kind: 'Name', value: 'weight' }, 279 + selectionSet: { 280 + kind: 'SelectionSet', 281 + selections: [ 282 + { 283 + kind: 'Field', 284 + name: { kind: 'Name', value: 'minimum' }, 285 + }, 286 + { 287 + kind: 'Field', 288 + name: { kind: 'Name', value: 'maximum' }, 289 + }, 290 + ], 291 + }, 292 + }, 293 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 294 + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 295 + ], 296 + }, 297 + }, 298 + ], 299 + }, 300 + }, 301 + { 302 + kind: 'FragmentDefinition', 303 + name: { kind: 'Name', value: 'pokemonFields' }, 304 + typeCondition: { 305 + kind: 'NamedType', 306 + name: { kind: 'Name', value: 'Pokemon' }, 307 + }, 308 + selectionSet: { 309 + kind: 'SelectionSet', 310 + selections: [ 311 + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 312 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 313 + { 314 + kind: 'Field', 315 + name: { kind: 'Name', value: 'attacks' }, 316 + selectionSet: { 317 + kind: 'SelectionSet', 318 + selections: [ 319 + { 320 + kind: 'Field', 321 + name: { kind: 'Name', value: 'fast' }, 322 + selectionSet: { 323 + kind: 'SelectionSet', 324 + selections: [ 325 + { 326 + kind: 'Field', 327 + name: { kind: 'Name', value: 'damage' }, 328 + }, 329 + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 330 + ], 331 + }, 332 + }, 333 + ], 334 + }, 335 + }, 336 + ], 337 + }, 338 + }, 339 + ], 340 + } as unknown as DocumentNode<PoQuery, PoQueryVariables>;
+2
test/e2e/fixture-project-unused-fields/gql/index.ts
··· 1 + export * from './fragment-masking'; 2 + export * from './gql';
+15
test/e2e/fixture-project-unused-fields/package.json
··· 1 + { 2 + "name": "fixtures", 3 + "private": true, 4 + "dependencies": { 5 + "graphql": "^16.0.0", 6 + "@graphql-typed-document-node/core": "^3.0.0", 7 + "@0no-co/graphqlsp": "workspace:*", 8 + "@urql/core": "^4.0.4", 9 + "urql": "^4.0.4" 10 + }, 11 + "devDependencies": { 12 + "@types/react": "18.2.45", 13 + "typescript": "^5.3.3" 14 + } 15 + }
+94
test/e2e/fixture-project-unused-fields/schema.graphql
··· 1 + ### This file was generated by Nexus Schema 2 + ### Do not make changes to this file directly 3 + 4 + """ 5 + Move a Pokémon can perform with the associated damage and type. 6 + """ 7 + type Attack { 8 + damage: Int 9 + name: String 10 + type: PokemonType 11 + } 12 + 13 + type AttacksConnection { 14 + fast: [Attack] 15 + special: [Attack] 16 + } 17 + 18 + """ 19 + Requirement that prevents an evolution through regular means of levelling up. 20 + """ 21 + type EvolutionRequirement { 22 + amount: Int 23 + name: String 24 + } 25 + 26 + type Pokemon { 27 + attacks: AttacksConnection 28 + classification: String @deprecated(reason: "And this is the reason why") 29 + evolutionRequirements: [EvolutionRequirement] 30 + evolutions: [Pokemon] 31 + 32 + """ 33 + Likelihood of an attempt to catch a Pokémon to fail. 34 + """ 35 + fleeRate: Float 36 + height: PokemonDimension 37 + id: ID! 38 + 39 + """ 40 + Maximum combat power a Pokémon may achieve at max level. 41 + """ 42 + maxCP: Int 43 + 44 + """ 45 + Maximum health points a Pokémon may achieve at max level. 46 + """ 47 + maxHP: Int 48 + name: String! 49 + resistant: [PokemonType] 50 + types: [PokemonType] 51 + weaknesses: [PokemonType] 52 + weight: PokemonDimension 53 + } 54 + 55 + type PokemonDimension { 56 + maximum: String 57 + minimum: String 58 + } 59 + 60 + """ 61 + Elemental property associated with either a Pokémon or one of their moves. 62 + """ 63 + enum PokemonType { 64 + Bug 65 + Dark 66 + Dragon 67 + Electric 68 + Fairy 69 + Fighting 70 + Fire 71 + Flying 72 + Ghost 73 + Grass 74 + Ground 75 + Ice 76 + Normal 77 + Poison 78 + Psychic 79 + Rock 80 + Steel 81 + Water 82 + } 83 + 84 + type Query { 85 + """ 86 + Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 + """ 88 + pokemon(id: ID!): Pokemon 89 + 90 + """ 91 + List out all Pokémon, optionally in pages 92 + """ 93 + pokemons(limit: Int, skip: Int): [Pokemon] 94 + }
+23
test/e2e/fixture-project-unused-fields/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "plugins": [ 4 + { 5 + "name": "@0no-co/graphqlsp", 6 + "schema": "./schema.graphql", 7 + "disableTypegen": true, 8 + "trackFieldUsage": true, 9 + "shouldCheckForColocatedFragments": false, 10 + "template": "graphql", 11 + "templateIsCallExpression": true 12 + } 13 + ], 14 + "target": "es2016", 15 + "jsx": "react-jsx", 16 + "esModuleInterop": true, 17 + "moduleResolution": "node", 18 + "forceConsistentCasingInFileNames": true, 19 + "strict": true, 20 + "skipLibCheck": true 21 + }, 22 + "exclude": ["node_modules", "fixtures"] 23 + }
+429
test/e2e/unused-fieds.test.ts
··· 1 + import { expect, afterAll, beforeAll, it, describe } from 'vitest'; 2 + import { TSServer } from './server'; 3 + import path from 'node:path'; 4 + import fs from 'node:fs'; 5 + import url from 'node:url'; 6 + import ts from 'typescript/lib/tsserverlibrary'; 7 + 8 + const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 + 10 + const projectPath = path.resolve(__dirname, 'fixture-project-unused-fields'); 11 + describe('unused fields', () => { 12 + const outfileDestructuringFromStart = path.join( 13 + projectPath, 14 + 'immediate-destructuring.tsx' 15 + ); 16 + const outfileDestructuring = path.join(projectPath, 'destructuring.tsx'); 17 + const outfileFragmentDestructuring = path.join( 18 + projectPath, 19 + 'fragment-destructuring.tsx' 20 + ); 21 + const outfileFragment = path.join(projectPath, 'fragment.tsx'); 22 + const outfilePropAccess = path.join(projectPath, 'property-access.tsx'); 23 + 24 + let server: TSServer; 25 + beforeAll(async () => { 26 + server = new TSServer(projectPath, { debugLog: false }); 27 + 28 + server.sendCommand('open', { 29 + file: outfileDestructuring, 30 + fileContent: '// empty', 31 + scriptKindName: 'TS', 32 + } satisfies ts.server.protocol.OpenRequestArgs); 33 + server.sendCommand('open', { 34 + file: outfileFragment, 35 + fileContent: '// empty', 36 + scriptKindName: 'TS', 37 + } satisfies ts.server.protocol.OpenRequestArgs); 38 + server.sendCommand('open', { 39 + file: outfilePropAccess, 40 + fileContent: '// empty', 41 + scriptKindName: 'TS', 42 + } satisfies ts.server.protocol.OpenRequestArgs); 43 + server.sendCommand('open', { 44 + file: outfileFragmentDestructuring, 45 + fileContent: '// empty', 46 + scriptKindName: 'TS', 47 + } satisfies ts.server.protocol.OpenRequestArgs); 48 + server.sendCommand('open', { 49 + file: outfileDestructuringFromStart, 50 + fileContent: '// empty', 51 + scriptKindName: 'TS', 52 + } satisfies ts.server.protocol.OpenRequestArgs); 53 + 54 + server.sendCommand('updateOpen', { 55 + openFiles: [ 56 + { 57 + file: outfileDestructuring, 58 + fileContent: fs.readFileSync( 59 + path.join(projectPath, 'fixtures/destructuring.tsx'), 60 + 'utf-8' 61 + ), 62 + }, 63 + { 64 + file: outfileFragment, 65 + fileContent: fs.readFileSync( 66 + path.join(projectPath, 'fixtures/fragment.tsx'), 67 + 'utf-8' 68 + ), 69 + }, 70 + { 71 + file: outfilePropAccess, 72 + fileContent: fs.readFileSync( 73 + path.join(projectPath, 'fixtures/property-access.tsx'), 74 + 'utf-8' 75 + ), 76 + }, 77 + { 78 + file: outfileDestructuringFromStart, 79 + fileContent: fs.readFileSync( 80 + path.join(projectPath, 'fixtures/immediate-destructuring.tsx'), 81 + 'utf-8' 82 + ), 83 + }, 84 + { 85 + file: outfileFragmentDestructuring, 86 + fileContent: fs.readFileSync( 87 + path.join(projectPath, 'fixtures/fragment-destructuring.tsx'), 88 + 'utf-8' 89 + ), 90 + }, 91 + ], 92 + } satisfies ts.server.protocol.UpdateOpenRequestArgs); 93 + 94 + server.sendCommand('saveto', { 95 + file: outfileDestructuring, 96 + tmpfile: outfileDestructuring, 97 + } satisfies ts.server.protocol.SavetoRequestArgs); 98 + server.sendCommand('saveto', { 99 + file: outfileFragment, 100 + tmpfile: outfileFragment, 101 + } satisfies ts.server.protocol.SavetoRequestArgs); 102 + server.sendCommand('saveto', { 103 + file: outfilePropAccess, 104 + tmpfile: outfilePropAccess, 105 + } satisfies ts.server.protocol.SavetoRequestArgs); 106 + server.sendCommand('saveto', { 107 + file: outfileFragmentDestructuring, 108 + tmpfile: outfileFragmentDestructuring, 109 + } satisfies ts.server.protocol.SavetoRequestArgs); 110 + server.sendCommand('saveto', { 111 + file: outfileDestructuringFromStart, 112 + tmpfile: outfileDestructuringFromStart, 113 + } satisfies ts.server.protocol.SavetoRequestArgs); 114 + }); 115 + 116 + afterAll(() => { 117 + try { 118 + fs.unlinkSync(outfileDestructuring); 119 + fs.unlinkSync(outfileFragment); 120 + fs.unlinkSync(outfilePropAccess); 121 + fs.unlinkSync(outfileFragmentDestructuring); 122 + fs.unlinkSync(outfileDestructuringFromStart); 123 + } catch {} 124 + }); 125 + 126 + it('gives unused fields with fragments', async () => { 127 + await server.waitForResponse( 128 + e => 129 + e.type === 'event' && 130 + e.event === 'semanticDiag' && 131 + e.body?.file === outfileFragment 132 + ); 133 + const res = server.responses.filter( 134 + resp => 135 + resp.type === 'event' && 136 + resp.event === 'semanticDiag' && 137 + resp.body?.file === outfileFragment 138 + ); 139 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 140 + [ 141 + { 142 + "category": "warning", 143 + "code": 52005, 144 + "end": { 145 + "line": 10, 146 + "offset": 15, 147 + }, 148 + "start": { 149 + "line": 10, 150 + "offset": 9, 151 + }, 152 + "text": "Field 'attacks.fast.damage' is not used.", 153 + }, 154 + { 155 + "category": "warning", 156 + "code": 52005, 157 + "end": { 158 + "line": 11, 159 + "offset": 13, 160 + }, 161 + "start": { 162 + "line": 11, 163 + "offset": 9, 164 + }, 165 + "text": "Field 'attacks.fast.name' is not used.", 166 + }, 167 + ] 168 + `); 169 + }, 30000); 170 + 171 + it('gives unused fields with fragments destructuring', async () => { 172 + await server.waitForResponse( 173 + e => 174 + e.type === 'event' && 175 + e.event === 'semanticDiag' && 176 + e.body?.file === outfileFragmentDestructuring 177 + ); 178 + const res = server.responses.filter( 179 + resp => 180 + resp.type === 'event' && 181 + resp.event === 'semanticDiag' && 182 + resp.body?.file === outfileFragmentDestructuring 183 + ); 184 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 185 + [ 186 + { 187 + "category": "warning", 188 + "code": 52005, 189 + "end": { 190 + "line": 10, 191 + "offset": 15, 192 + }, 193 + "start": { 194 + "line": 10, 195 + "offset": 9, 196 + }, 197 + "text": "Field 'attacks.fast.damage' is not used.", 198 + }, 199 + { 200 + "category": "warning", 201 + "code": 52005, 202 + "end": { 203 + "line": 11, 204 + "offset": 13, 205 + }, 206 + "start": { 207 + "line": 11, 208 + "offset": 9, 209 + }, 210 + "text": "Field 'attacks.fast.name' is not used.", 211 + }, 212 + ] 213 + `); 214 + }, 30000); 215 + 216 + it('gives semantc diagnostics with property access', async () => { 217 + await server.waitForResponse( 218 + e => 219 + e.type === 'event' && 220 + e.event === 'semanticDiag' && 221 + e.body?.file === outfilePropAccess 222 + ); 223 + const res = server.responses.filter( 224 + resp => 225 + resp.type === 'event' && 226 + resp.event === 'semanticDiag' && 227 + resp.body?.file === outfilePropAccess 228 + ); 229 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 230 + [ 231 + { 232 + "category": "warning", 233 + "code": 52005, 234 + "end": { 235 + "line": 11, 236 + "offset": 15, 237 + }, 238 + "start": { 239 + "line": 11, 240 + "offset": 7, 241 + }, 242 + "text": "Field 'pokemon.fleeRate' is not used.", 243 + }, 244 + { 245 + "category": "warning", 246 + "code": 52005, 247 + "end": { 248 + "line": 16, 249 + "offset": 17, 250 + }, 251 + "start": { 252 + "line": 16, 253 + "offset": 11, 254 + }, 255 + "text": "Field 'pokemon.attacks.special.damage' is not used.", 256 + }, 257 + { 258 + "category": "warning", 259 + "code": 52005, 260 + "end": { 261 + "line": 20, 262 + "offset": 16, 263 + }, 264 + "start": { 265 + "line": 20, 266 + "offset": 9, 267 + }, 268 + "text": "Field 'pokemon.weight.minimum' is not used.", 269 + }, 270 + { 271 + "category": "warning", 272 + "code": 52005, 273 + "end": { 274 + "line": 21, 275 + "offset": 16, 276 + }, 277 + "start": { 278 + "line": 21, 279 + "offset": 9, 280 + }, 281 + "text": "Field 'pokemon.weight.maximum' is not used.", 282 + }, 283 + { 284 + "category": "error", 285 + "code": 2578, 286 + "end": { 287 + "line": 3, 288 + "offset": 20, 289 + }, 290 + "start": { 291 + "line": 3, 292 + "offset": 1, 293 + }, 294 + "text": "Unused '@ts-expect-error' directive.", 295 + }, 296 + ] 297 + `); 298 + }, 30000); 299 + 300 + it('gives unused fields with destructuring', async () => { 301 + const res = server.responses.filter( 302 + resp => 303 + resp.type === 'event' && 304 + resp.event === 'semanticDiag' && 305 + resp.body?.file === outfileDestructuring 306 + ); 307 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 308 + [ 309 + { 310 + "category": "warning", 311 + "code": 52005, 312 + "end": { 313 + "line": 15, 314 + "offset": 15, 315 + }, 316 + "start": { 317 + "line": 15, 318 + "offset": 11, 319 + }, 320 + "text": "Field 'pokemon.attacks.special.name' is not used.", 321 + }, 322 + { 323 + "category": "warning", 324 + "code": 52005, 325 + "end": { 326 + "line": 16, 327 + "offset": 17, 328 + }, 329 + "start": { 330 + "line": 16, 331 + "offset": 11, 332 + }, 333 + "text": "Field 'pokemon.attacks.special.damage' is not used.", 334 + }, 335 + { 336 + "category": "warning", 337 + "code": 52005, 338 + "end": { 339 + "line": 23, 340 + "offset": 11, 341 + }, 342 + "start": { 343 + "line": 23, 344 + "offset": 7, 345 + }, 346 + "text": "Field 'pokemon.name' is not used.", 347 + }, 348 + { 349 + "category": "error", 350 + "code": 2578, 351 + "end": { 352 + "line": 3, 353 + "offset": 20, 354 + }, 355 + "start": { 356 + "line": 3, 357 + "offset": 1, 358 + }, 359 + "text": "Unused '@ts-expect-error' directive.", 360 + }, 361 + ] 362 + `); 363 + }, 30000); 364 + 365 + it('gives unused fields with immedaite destructuring', async () => { 366 + const res = server.responses.filter( 367 + resp => 368 + resp.type === 'event' && 369 + resp.event === 'semanticDiag' && 370 + resp.body?.file === outfileDestructuringFromStart 371 + ); 372 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 373 + [ 374 + { 375 + "category": "warning", 376 + "code": 52005, 377 + "end": { 378 + "line": 15, 379 + "offset": 15, 380 + }, 381 + "start": { 382 + "line": 15, 383 + "offset": 11, 384 + }, 385 + "text": "Field 'pokemon.attacks.special.name' is not used.", 386 + }, 387 + { 388 + "category": "warning", 389 + "code": 52005, 390 + "end": { 391 + "line": 16, 392 + "offset": 17, 393 + }, 394 + "start": { 395 + "line": 16, 396 + "offset": 11, 397 + }, 398 + "text": "Field 'pokemon.attacks.special.damage' is not used.", 399 + }, 400 + { 401 + "category": "warning", 402 + "code": 52005, 403 + "end": { 404 + "line": 23, 405 + "offset": 11, 406 + }, 407 + "start": { 408 + "line": 23, 409 + "offset": 7, 410 + }, 411 + "text": "Field 'pokemon.name' is not used.", 412 + }, 413 + { 414 + "category": "error", 415 + "code": 2578, 416 + "end": { 417 + "line": 3, 418 + "offset": 20, 419 + }, 420 + "start": { 421 + "line": 3, 422 + "offset": 1, 423 + }, 424 + "text": "Unused '@ts-expect-error' directive.", 425 + }, 426 + ] 427 + `); 428 + }, 30000); 429 + });