···
export const UNUSED_FIELD_CODE = 52005;
8
-
const getVariableDeclaration = (start: ts.NoSubstitutionTemplateLiteral) => {
9
-
let node: any = start;
11
-
while (!ts.isVariableDeclaration(node) && node.parent && counter < 5) {
8
+
const unwrapAbstractType = (type: ts.Type) => {
9
+
return type.isUnionOrIntersection()
10
+
? type.types.find(type => type.flags & ts.TypeFlags.Object) || type
14
+
const getVariableDeclaration = (
16
+
): ts.VariableDeclaration | undefined => {
17
+
let node: ts.Node = start;
18
+
const seen = new Set();
19
+
while (node.parent && !seen.has(node)) {
21
+
if (ts.isBlock(node)) {
22
+
return; // NOTE: We never want to traverse up into a new function/module block
23
+
} else if (ts.isVariableDeclaration((node = node.parent))) {
const traverseArrayDestructuring = (
···
110
-
node: ts.Identifier | ts.BindingName,
122
+
node: ts.BindingName,
originalWip: Array<string>,
allFields: Array<string>,
info: ts.server.PluginCreateInfo
128
+
if (ts.isObjectBindingPattern(node)) {
129
+
return traverseDestructuring(node, originalWip, allFields, source, info);
130
+
} else if (ts.isArrayBindingPattern(node)) {
131
+
return traverseArrayDestructuring(
let results: string[] = [];
const references = info.languageService.getReferencesAtPosition(
···
ts.isBinaryExpression(foundRef)
if (ts.isVariableDeclaration(foundRef)) {
158
-
if (ts.isIdentifier(foundRef.name)) {
159
-
// We have already added the paths because of the right-hand expression,
160
-
// const pokemon = result.data.pokemon --> we have pokemon as our path,
161
-
// now re-crawling pokemon for all of its accessors should deliver us the usage
162
-
// patterns... This might get expensive though if we need to perform this deeply.
163
-
return crawlScope(foundRef.name, pathParts, allFields, source, info);
164
-
} else if (ts.isObjectBindingPattern(foundRef.name)) {
165
-
// First we need to traverse the left-hand side of the variable assignment,
166
-
// this could be tree-like as we could be dealing with
167
-
// - const { x: { y: z }, a: { b: { c, d }, e: { f } } } = result.data
168
-
// Which we will need several paths for...
169
-
// after doing that we need to re-crawl all of the resulting variables
170
-
// Crawl down until we have either a leaf node or an object/array that can
172
-
return traverseDestructuring(
179
-
} else if (ts.isArrayBindingPattern(foundRef.name)) {
180
-
return traverseArrayDestructuring(
182
+
return crawlScope(foundRef.name, pathParts, allFields, source, info);
ts.isIdentifier(foundRef) &&
!pathParts.includes(foundRef.text)
···
foundRef.name.text === 'every' || foundRef.name.text === 'some';
const callExpression = foundRef.parent;
211
-
const func = callExpression.arguments[0];
212
-
if (ts.isFunctionExpression(func) || ts.isArrowFunction(func)) {
206
+
let func: ts.Expression | ts.FunctionDeclaration =
207
+
callExpression.arguments[0];
209
+
if (ts.isIdentifier(func)) {
210
+
// TODO: Scope utilities in checkFieldUsageInFile to deduplicate
211
+
const checker = info.languageService.getProgram()!.getTypeChecker();
213
+
const declaration =
214
+
checker.getSymbolAtLocation(func)?.valueDeclaration;
215
+
if (declaration && ts.isFunctionDeclaration(declaration)) {
216
+
func = declaration;
219
+
ts.isVariableDeclaration(declaration) &&
220
+
declaration.initializer
222
+
func = declaration.initializer;
227
+
ts.isFunctionDeclaration(func) ||
228
+
ts.isFunctionExpression(func) ||
229
+
ts.isArrowFunction(func)
const param = func.parameters[isReduce ? 1 : 0];
···
237
-
} else if (ts.isIdentifier(func)) {
238
-
// TODO: get the function and do the same as the above
ts.isPropertyAccessExpression(foundRef) &&
···
const defaultReservedKeys = ['id', '_id', '__typename'];
const additionalKeys = info.config.reservedKeys ?? [];
const reservedKeys = new Set([...defaultReservedKeys, ...additionalKeys]);
302
+
const checker = info.languageService.getProgram()?.getTypeChecker();
303
+
if (!checker) return;
···
const variableDeclaration = getVariableDeclaration(node);
296
-
if (!ts.isVariableDeclaration(variableDeclaration)) return;
314
+
if (!variableDeclaration) return;
316
+
let dataType: ts.Type | undefined;
318
+
const type = checker.getTypeAtLocation(node.parent) as
321
+
// Attempt to retrieve type from internally resolve type arguments
322
+
if ('target' in type) {
323
+
const typeArguments = (type as any)
324
+
.resolvedTypeArguments as readonly ts.Type[];
325
+
dataType = typeArguments.length > 1 ? typeArguments[0] : undefined;
327
+
// Fallback to resolving the type from scratch
329
+
const apiTypeSymbol = type.getProperty('__apiType');
330
+
if (apiTypeSymbol) {
331
+
let apiType = checker.getTypeOfSymbol(apiTypeSymbol);
332
+
let callSignature: ts.Signature | undefined =
333
+
type.getCallSignatures()[0];
334
+
if (apiType.isUnionOrIntersection()) {
335
+
for (const type of apiType.types) {
336
+
callSignature = type.getCallSignatures()[0];
337
+
if (callSignature) {
338
+
dataType = callSignature.getReturnType();
343
+
dataType = callSignature && callSignature.getReturnType();
const references = info.languageService.getReferencesAtPosition(
variableDeclaration.name.getStart()
const allAccess: string[] = [];
···
references.forEach(ref => {
if (ref.fileName !== source.fileName) return;
344
-
let found = findNode(source, ref.textSpan.start);
345
-
while (found && !ts.isVariableStatement(found)) {
346
-
found = found.parent;
394
+
const targetNode = findNode(source, ref.textSpan.start);
395
+
if (!targetNode) return;
396
+
// Skip declaration as reference of itself
397
+
if (targetNode.parent === variableDeclaration) return;
349
-
if (!found || !ts.isVariableStatement(found)) return;
399
+
const scopeSymbols = checker.getSymbolsInScope(
401
+
ts.SymbolFlags.BlockScopedVariable
351
-
const [output] = found.declarationList.declarations;
404
+
let scopeDataSymbol: ts.Symbol | undefined;
405
+
for (let scopeSymbol of scopeSymbols) {
406
+
if (!scopeSymbol.valueDeclaration) continue;
407
+
let typeOfScopeSymbol = unwrapAbstractType(
408
+
checker.getTypeOfSymbol(scopeSymbol)
410
+
if (dataType === typeOfScopeSymbol) {
411
+
scopeDataSymbol = scopeSymbol;
353
-
if (output.name.getText() === variableDeclaration.name.getText())
415
+
// NOTE: This is an aggressive fallback for hooks where the return value isn't destructured
416
+
// This is a last resort solution for patterns like react-query, where the fallback that
417
+
// would otherwise happen below isn't sufficient
418
+
if (typeOfScopeSymbol.flags & ts.TypeFlags.Object) {
419
+
const tuplePropertySymbol = typeOfScopeSymbol.getProperty('0');
420
+
if (tuplePropertySymbol) {
421
+
typeOfScopeSymbol = checker.getTypeOfSymbol(tuplePropertySymbol);
422
+
if (dataType === typeOfScopeSymbol) {
423
+
scopeDataSymbol = scopeSymbol;
428
+
const dataPropertySymbol = typeOfScopeSymbol.getProperty('data');
429
+
if (dataPropertySymbol) {
430
+
typeOfScopeSymbol = unwrapAbstractType(
431
+
checker.getTypeOfSymbol(dataPropertySymbol)
433
+
if (dataType === typeOfScopeSymbol) {
434
+
scopeDataSymbol = scopeSymbol;
356
-
let temp = output.name;
357
-
// Supported cases:
358
-
// - const result = await client.query() || useFragment()
359
-
// - const [result] = useQuery() --> urql
360
-
// - const { data } = useQuery() --> Apollo
361
-
// - const { field } = useFragment()
362
-
// - const [{ data }] = useQuery()
363
-
// - const { data: { pokemon } } = useQuery()
441
+
const valueDeclaration = scopeDataSymbol?.valueDeclaration;
442
+
let name: ts.BindingName | undefined;
365
-
ts.isArrayBindingPattern(temp) &&
366
-
ts.isBindingElement(temp.elements[0])
444
+
valueDeclaration &&
445
+
'name' in valueDeclaration &&
446
+
!!valueDeclaration.name &&
447
+
(ts.isIdentifier(valueDeclaration.name as any) ||
448
+
ts.isBindingName(valueDeclaration.name as any))
368
-
temp = temp.elements[0].name;
450
+
name = valueDeclaration.name as ts.BindingName;
452
+
// Fall back to looking at the variable declaration directly,
453
+
// if we are on one.
454
+
const variableDeclaration = getVariableDeclaration(targetNode);
455
+
if (variableDeclaration) name = variableDeclaration.name;
371
-
if (ts.isObjectBindingPattern(temp)) {
372
-
const result = traverseDestructuring(
379
-
allAccess.push(...result);
381
-
const result = crawlScope(temp, [], allPaths, source, info);
459
+
const result = crawlScope(name, [], allPaths, source, info);
allAccess.push(...result);
386
-
// Bail when we can't find anything
387
-
if (!allAccess.length) return;
464
+
if (!allAccess.length) {
const unused = allPaths.filter(x => !allAccess.includes(x));