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

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

authored by

Jovi De Croock and committed by
GitHub
39f573d1 38b02186

+125 -10
+5
.changeset/loud-dryers-burn.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Add a bail for `fieldUsage` where we return a property from a function
+4
packages/example-tada/introspection.ts
··· 435 435 { 436 436 "kind": "SCALAR", 437 437 "name": "Boolean" 438 + }, 439 + { 440 + "kind": "SCALAR", 441 + "name": "Any" 438 442 } 439 443 ], 440 444 "directives": []
+32 -10
packages/graphqlsp/src/fieldUsage.ts
··· 38 38 39 39 const wip = [...originalWip]; 40 40 return ts.isIdentifier(element.name) 41 - ? crawlScope(element.name, wip, allFields, source, info) 41 + ? crawlScope(element.name, wip, allFields, source, info, false) 42 42 : ts.isObjectBindingPattern(element.name) 43 43 ? traverseDestructuring(element.name, wip, allFields, source, info) 44 44 : traverseArrayDestructuring(element.name, wip, allFields, source, info); ··· 96 96 wip, 97 97 allFields, 98 98 source, 99 - info 99 + info, 100 + false 100 101 ); 101 102 102 103 results.push(...crawlResult); ··· 123 124 originalWip: Array<string>, 124 125 allFields: Array<string>, 125 126 source: ts.SourceFile, 126 - info: ts.server.PluginCreateInfo 127 + info: ts.server.PluginCreateInfo, 128 + inArrayMethod: boolean 127 129 ): Array<string> => { 128 130 if (ts.isObjectBindingPattern(node)) { 129 131 return traverseDestructuring(node, originalWip, allFields, source, info); ··· 176 178 ts.isPropertyAccessExpression(foundRef) || 177 179 ts.isElementAccessExpression(foundRef) || 178 180 ts.isVariableDeclaration(foundRef) || 179 - ts.isBinaryExpression(foundRef) 181 + ts.isBinaryExpression(foundRef) || 182 + ts.isReturnStatement(foundRef) || 183 + ts.isArrowFunction(foundRef) 180 184 ) { 181 - if (ts.isVariableDeclaration(foundRef)) { 182 - return crawlScope(foundRef.name, pathParts, allFields, source, info); 185 + if ( 186 + !inArrayMethod && 187 + (ts.isReturnStatement(foundRef) || ts.isArrowFunction(foundRef)) 188 + ) { 189 + // When we are returning the ref or we are dealing with an implicit return 190 + // we mark all its children as used (bail scenario) 191 + const joined = pathParts.join('.'); 192 + const bailedFields = allFields.filter(x => x.startsWith(joined + '.')); 193 + return bailedFields; 194 + } else if (ts.isVariableDeclaration(foundRef)) { 195 + return crawlScope( 196 + foundRef.name, 197 + pathParts, 198 + allFields, 199 + source, 200 + info, 201 + false 202 + ); 183 203 } else if ( 184 204 ts.isIdentifier(foundRef) && 185 205 !pathParts.includes(foundRef.text) 186 206 ) { 187 207 const joined = [...pathParts, foundRef.text].join('.'); 188 - if (allFields.find(x => x.startsWith(joined))) { 208 + if (allFields.find(x => x.startsWith(joined + '.'))) { 189 209 pathParts.push(foundRef.text); 190 210 } 191 211 } else if ( ··· 234 254 pathParts, 235 255 allFields, 236 256 source, 237 - info 257 + info, 258 + true 238 259 ); 239 260 240 261 if ( ··· 246 267 pathParts, 247 268 allFields, 248 269 source, 249 - info 270 + info, 271 + true 250 272 ); 251 273 res.push(...varRes); 252 274 } ··· 458 480 } 459 481 460 482 if (name) { 461 - const result = crawlScope(name, [], allPaths, source, info); 483 + const result = crawlScope(name, [], allPaths, source, info, false); 462 484 allAccess.push(...result); 463 485 } 464 486 });
+40
test/e2e/fixture-project-unused-fields/fixtures/bail.tsx
··· 1 + import * as React from 'react'; 2 + import { useQuery } from 'urql'; 3 + import { graphql } from './gql'; 4 + // @ts-expect-error 5 + import { Pokemon } from './fragment'; 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 = React.useMemo(() => result.data?.pokemon, []) 36 + 37 + // @ts-expect-error 38 + return <Pokemon data={result.data?.pokemon} />; 39 + } 40 +
+44
test/e2e/unused-fieds.test.ts
··· 14 14 'immediate-destructuring.tsx' 15 15 ); 16 16 const outfileDestructuring = path.join(projectPath, 'destructuring.tsx'); 17 + const outfileBail = path.join(projectPath, 'bail.tsx'); 17 18 const outfileFragmentDestructuring = path.join( 18 19 projectPath, 19 20 'fragment-destructuring.tsx' ··· 27 28 28 29 server.sendCommand('open', { 29 30 file: outfileDestructuring, 31 + fileContent: '// empty', 32 + scriptKindName: 'TS', 33 + } satisfies ts.server.protocol.OpenRequestArgs); 34 + server.sendCommand('open', { 35 + file: outfileBail, 30 36 fileContent: '// empty', 31 37 scriptKindName: 'TS', 32 38 } satisfies ts.server.protocol.OpenRequestArgs); ··· 61 67 ), 62 68 }, 63 69 { 70 + file: outfileBail, 71 + fileContent: fs.readFileSync( 72 + path.join(projectPath, 'fixtures/bail.tsx'), 73 + 'utf-8' 74 + ), 75 + }, 76 + { 64 77 file: outfileFragment, 65 78 fileContent: fs.readFileSync( 66 79 path.join(projectPath, 'fixtures/fragment.tsx'), ··· 111 124 file: outfileDestructuringFromStart, 112 125 tmpfile: outfileDestructuringFromStart, 113 126 } satisfies ts.server.protocol.SavetoRequestArgs); 127 + server.sendCommand('saveto', { 128 + file: outfileBail, 129 + tmpfile: outfileBail, 130 + } satisfies ts.server.protocol.SavetoRequestArgs); 114 131 }); 115 132 116 133 afterAll(() => { ··· 120 137 fs.unlinkSync(outfilePropAccess); 121 138 fs.unlinkSync(outfileFragmentDestructuring); 122 139 fs.unlinkSync(outfileDestructuringFromStart); 140 + fs.unlinkSync(outfileBail); 123 141 } catch {} 124 142 }); 125 143 ··· 354 372 }, 355 373 "start": { 356 374 "line": 3, 375 + "offset": 1, 376 + }, 377 + "text": "Unused '@ts-expect-error' directive.", 378 + }, 379 + ] 380 + `); 381 + }, 30000); 382 + 383 + it('Bails unused fields when memo func is used', async () => { 384 + const res = server.responses.filter( 385 + resp => 386 + resp.type === 'event' && 387 + resp.event === 'semanticDiag' && 388 + resp.body?.file === outfileBail 389 + ); 390 + expect(res[0].body.diagnostics).toMatchInlineSnapshot(` 391 + [ 392 + { 393 + "category": "error", 394 + "code": 2578, 395 + "end": { 396 + "line": 4, 397 + "offset": 20, 398 + }, 399 + "start": { 400 + "line": 4, 357 401 "offset": 1, 358 402 }, 359 403 "text": "Unused '@ts-expect-error' directive.",