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.

refactor(graphqlsp): Add declaration helpers to replace language services (#351)

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

authored by

Phil Pluckthun
Jovi De Croock
and committed by
GitHub
c961c171 180702b2

+481 -169
+5
.changeset/puny-ghosts-clap.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Add new value declaration helpers to replace built-in services and to traverse TypeScript type checked AST exhaustively and efficiently.
+353
packages/graphqlsp/src/ast/declaration.ts
··· 1 + import { ts } from '../ts'; 2 + 3 + export type ValueDeclaration = 4 + | ts.BinaryExpression 5 + | ts.ArrowFunction 6 + | ts.BindingElement 7 + | ts.ClassDeclaration 8 + | ts.ClassExpression 9 + | ts.ClassStaticBlockDeclaration 10 + | ts.ConstructorDeclaration 11 + | ts.EnumDeclaration 12 + | ts.EnumMember 13 + | ts.ExportSpecifier 14 + | ts.FunctionDeclaration 15 + | ts.FunctionExpression 16 + | ts.GetAccessorDeclaration 17 + | ts.JsxAttribute 18 + | ts.MethodDeclaration 19 + | ts.ModuleDeclaration 20 + | ts.ParameterDeclaration 21 + | ts.PropertyAssignment 22 + | ts.PropertyDeclaration 23 + | ts.SetAccessorDeclaration 24 + | ts.ShorthandPropertyAssignment 25 + | ts.VariableDeclaration; 26 + 27 + export type ValueOfDeclaration = 28 + | ts.ClassExpression 29 + | ts.ClassDeclaration 30 + | ts.ArrowFunction 31 + | ts.ClassStaticBlockDeclaration 32 + | ts.ConstructorDeclaration 33 + | ts.EnumDeclaration 34 + | ts.FunctionDeclaration 35 + | ts.GetAccessorDeclaration 36 + | ts.SetAccessorDeclaration 37 + | ts.MethodDeclaration 38 + | ts.Expression; 39 + 40 + /** Checks if a node is a `ts.Declaration` and a value. 41 + * @remarks 42 + * This checks if a given node is a value declaration only, 43 + * excluding import/export specifiers, type declarations, and 44 + * ambient declarations. 45 + * All declarations that aren't JS(x) nodes will be discarded. 46 + * This is based on `ts.isDeclarationKind`. 47 + */ 48 + export function isValueDeclaration(node: ts.Node): node is ValueDeclaration { 49 + switch (node.kind) { 50 + case ts.SyntaxKind.BinaryExpression: 51 + case ts.SyntaxKind.ArrowFunction: 52 + case ts.SyntaxKind.BindingElement: 53 + case ts.SyntaxKind.ClassDeclaration: 54 + case ts.SyntaxKind.ClassExpression: 55 + case ts.SyntaxKind.ClassStaticBlockDeclaration: 56 + case ts.SyntaxKind.Constructor: 57 + case ts.SyntaxKind.EnumDeclaration: 58 + case ts.SyntaxKind.EnumMember: 59 + case ts.SyntaxKind.FunctionDeclaration: 60 + case ts.SyntaxKind.FunctionExpression: 61 + case ts.SyntaxKind.GetAccessor: 62 + case ts.SyntaxKind.JsxAttribute: 63 + case ts.SyntaxKind.MethodDeclaration: 64 + case ts.SyntaxKind.Parameter: 65 + case ts.SyntaxKind.PropertyAssignment: 66 + case ts.SyntaxKind.PropertyDeclaration: 67 + case ts.SyntaxKind.SetAccessor: 68 + case ts.SyntaxKind.ShorthandPropertyAssignment: 69 + case ts.SyntaxKind.VariableDeclaration: 70 + return true; 71 + default: 72 + return false; 73 + } 74 + } 75 + 76 + /** Returns true if operator assigns a value unchanged */ 77 + function isAssignmentOperator(token: ts.BinaryOperatorToken): boolean { 78 + switch (token.kind) { 79 + case ts.SyntaxKind.EqualsToken: 80 + case ts.SyntaxKind.BarBarEqualsToken: 81 + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: 82 + case ts.SyntaxKind.QuestionQuestionEqualsToken: 83 + return true; 84 + default: 85 + return false; 86 + } 87 + } 88 + 89 + /** Evaluates to the declaration's value initializer or itself if it declares a value */ 90 + export function getValueOfValueDeclaration( 91 + node: ValueDeclaration 92 + ): ValueOfDeclaration | undefined { 93 + switch (node.kind) { 94 + case ts.SyntaxKind.ClassExpression: 95 + case ts.SyntaxKind.ClassDeclaration: 96 + case ts.SyntaxKind.ArrowFunction: 97 + case ts.SyntaxKind.ClassStaticBlockDeclaration: 98 + case ts.SyntaxKind.Constructor: 99 + case ts.SyntaxKind.EnumDeclaration: 100 + case ts.SyntaxKind.FunctionDeclaration: 101 + case ts.SyntaxKind.FunctionExpression: 102 + case ts.SyntaxKind.GetAccessor: 103 + case ts.SyntaxKind.SetAccessor: 104 + case ts.SyntaxKind.MethodDeclaration: 105 + return node; 106 + case ts.SyntaxKind.BindingElement: 107 + case ts.SyntaxKind.EnumMember: 108 + case ts.SyntaxKind.JsxAttribute: 109 + case ts.SyntaxKind.Parameter: 110 + case ts.SyntaxKind.PropertyAssignment: 111 + case ts.SyntaxKind.PropertyDeclaration: 112 + case ts.SyntaxKind.VariableDeclaration: 113 + return node.initializer; 114 + case ts.SyntaxKind.BinaryExpression: 115 + return isAssignmentOperator(node.operatorToken) ? node.right : undefined; 116 + case ts.SyntaxKind.ShorthandPropertyAssignment: 117 + return node.objectAssignmentInitializer; 118 + default: 119 + return undefined; 120 + } 121 + } 122 + 123 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L652-L654 124 + function climbPastPropertyOrElementAccess(node: ts.Node): ts.Node { 125 + if ( 126 + node.parent && 127 + ts.isPropertyAccessExpression(node.parent) && 128 + node.parent.name === node 129 + ) { 130 + return node.parent; 131 + } else if ( 132 + node.parent && 133 + ts.isElementAccessExpression(node.parent) && 134 + node.parent.argumentExpression === node 135 + ) { 136 + return node.parent; 137 + } else { 138 + return node; 139 + } 140 + } 141 + 142 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L602-L605 143 + function isNewExpressionTarget(node: ts.Node): node is ts.NewExpression { 144 + const target = climbPastPropertyOrElementAccess(node).parent; 145 + return ts.isNewExpression(target) && target.expression === node; 146 + } 147 + 148 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L607-L610 149 + function isCallOrNewExpressionTarget( 150 + node: ts.Node 151 + ): node is ts.CallExpression | ts.NewExpression { 152 + const target = climbPastPropertyOrElementAccess(node).parent; 153 + return ts.isCallOrNewExpression(target) && target.expression === node; 154 + } 155 + 156 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L716-L719 157 + function isNameOfFunctionDeclaration(node: ts.Node): boolean { 158 + return ( 159 + ts.isIdentifier(node) && 160 + node.parent && 161 + ts.isFunctionLike(node.parent) && 162 + node.parent.name === node 163 + ); 164 + } 165 + 166 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L2441-L2447 167 + function getNameFromPropertyName(name: ts.PropertyName): string | undefined { 168 + if (ts.isComputedPropertyName(name)) { 169 + return ts.isStringLiteralLike(name.expression) || 170 + ts.isNumericLiteral(name.expression) 171 + ? name.expression.text 172 + : undefined; 173 + } else if (ts.isPrivateIdentifier(name) || ts.isMemberName(name)) { 174 + return ts.idText(name); 175 + } else { 176 + return name.text; 177 + } 178 + } 179 + 180 + /** Resolves the declaration of an identifier. 181 + * @remarks 182 + * This returns the declaration node first found for an identifier by resolving an identifier's 183 + * symbol via the type checker. 184 + * @privateRemarks 185 + * This mirrors the implementation of `getDefinitionAtPosition` in TS' language service. However, 186 + * it removes all cases that aren't applicable to identifiers and removes the intermediary positional 187 + * data structure, instead returning raw AST nodes. 188 + */ 189 + export function getDeclarationOfIdentifier( 190 + node: ts.Identifier, 191 + checker: ts.TypeChecker 192 + ): ValueDeclaration | undefined { 193 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L523-L540 194 + let symbol = checker.getSymbolAtLocation(node); 195 + if ( 196 + symbol?.declarations?.[0] && 197 + symbol.flags & ts.SymbolFlags.Alias && 198 + (node.parent === symbol?.declarations?.[0] || 199 + !ts.isNamespaceImport(symbol.declarations[0])) 200 + ) { 201 + // Resolve alias symbols, excluding self-referential symbols 202 + const aliased = checker.getAliasedSymbol(symbol); 203 + if (aliased.declarations) symbol = aliased; 204 + } 205 + 206 + if (symbol && ts.isShorthandPropertyAssignment(node.parent)) { 207 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L248-L257 208 + // Resolve shorthand property assignments 209 + const shorthandSymbol = checker.getShorthandAssignmentValueSymbol( 210 + symbol.valueDeclaration 211 + ); 212 + if (shorthandSymbol) symbol = shorthandSymbol; 213 + } else if ( 214 + ts.isBindingElement(node.parent) && 215 + ts.isObjectBindingPattern(node.parent.parent) && 216 + node === (node.parent.propertyName || node.parent.name) 217 + ) { 218 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L259-L280 219 + // Resolve symbol of property in shorthand assignments 220 + const name = getNameFromPropertyName(node); 221 + const prop = name 222 + ? checker.getTypeAtLocation(node.parent.parent).getProperty(name) 223 + : undefined; 224 + if (prop) symbol = prop; 225 + } else if ( 226 + ts.isObjectLiteralElement(node.parent) && 227 + (ts.isObjectLiteralExpression(node.parent.parent) || 228 + ts.isJsxAttributes(node.parent.parent)) && 229 + node.parent.name === node 230 + ) { 231 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L298-L316 232 + // Resolve symbol of property in object literal destructre expressions 233 + const name = getNameFromPropertyName(node); 234 + const prop = name 235 + ? checker.getContextualType(node.parent.parent)?.getProperty(name) 236 + : undefined; 237 + if (prop) symbol = prop; 238 + } 239 + 240 + if (symbol && symbol.declarations?.length) { 241 + if ( 242 + symbol.flags & ts.SymbolFlags.Class && 243 + !(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) && 244 + isNewExpressionTarget(node) 245 + ) { 246 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L603-L610 247 + // Resolve first class-like declaration for new expressions 248 + for (const declaration of symbol.declarations) { 249 + if (ts.isClassLike(declaration)) return declaration; 250 + } 251 + } else if ( 252 + isCallOrNewExpressionTarget(node) || 253 + isNameOfFunctionDeclaration(node) 254 + ) { 255 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L612-L616 256 + // Resolve first function-like declaration for call expressions or named functions 257 + for (const declaration of symbol.declarations) { 258 + if ( 259 + ts.isFunctionLike(declaration) && 260 + !!(declaration as ts.FunctionLikeDeclaration).body && 261 + isValueDeclaration(declaration) 262 + ) { 263 + return declaration; 264 + } 265 + } 266 + } 267 + 268 + // Account for assignments to property access expressions 269 + // This resolves property access expressions to binding element parents 270 + if ( 271 + symbol.valueDeclaration && 272 + ts.isPropertyAccessExpression(symbol.valueDeclaration) 273 + ) { 274 + const parent = symbol.valueDeclaration.parent; 275 + if ( 276 + parent && 277 + ts.isBinaryExpression(parent) && 278 + parent.left === symbol.valueDeclaration 279 + ) { 280 + return parent; 281 + } 282 + } 283 + 284 + if ( 285 + symbol.valueDeclaration && 286 + isValueDeclaration(symbol.valueDeclaration) 287 + ) { 288 + // NOTE: We prefer value declarations, since the checker may have already applied conditions 289 + // similar to `isValueDeclaration` and selected it beforehand 290 + // Only use value declarations if they're not type/ambient declarations or imports/exports 291 + return symbol.valueDeclaration; 292 + } 293 + 294 + // Selecting the first available result, if any 295 + // NOTE: We left out `!isExpandoDeclaration` as a condition, since `valueDeclaration` above 296 + // should handle some of these cases, and we don't have to care about this subtlety as much for identifiers 297 + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L582-L590 298 + for (const declaration of symbol.declarations) { 299 + // Only use declarations if they're not type/ambient declarations or imports/exports 300 + if (isValueDeclaration(declaration)) return declaration; 301 + } 302 + } 303 + 304 + return undefined; 305 + } 306 + 307 + /** Loops {@link getDeclarationOfIdentifier} until a value of the identifier is found */ 308 + export function getValueOfIdentifier( 309 + node: ts.Identifier, 310 + checker: ts.TypeChecker 311 + ): ValueOfDeclaration | undefined { 312 + while (ts.isIdentifier(node)) { 313 + const declaration = getDeclarationOfIdentifier(node, checker); 314 + if (!declaration) { 315 + return undefined; 316 + } else { 317 + const value = getValueOfValueDeclaration(declaration); 318 + if (value && ts.isIdentifier(value) && value !== node) { 319 + // If the resolved value is another identifiers, we continue searching, if the 320 + // identifier isn't self-referential 321 + node = value; 322 + } else { 323 + return value; 324 + } 325 + } 326 + } 327 + } 328 + 329 + /** Resolves exressions that might not influence the target identifier */ 330 + export function getIdentifierOfChainExpression( 331 + node: ts.Expression 332 + ): ts.Identifier | undefined { 333 + let target: ts.Expression | undefined = node; 334 + while (target) { 335 + if (ts.isPropertyAccessExpression(target)) { 336 + target = target.name; 337 + } else if ( 338 + ts.isAsExpression(target) || 339 + ts.isSatisfiesExpression(target) || 340 + ts.isNonNullExpression(target) || 341 + ts.isParenthesizedExpression(target) || 342 + ts.isExpressionWithTypeArguments(target) 343 + ) { 344 + target = target.expression; 345 + } else if (ts.isCommaListExpression(target)) { 346 + target = target.elements[target.elements.length - 1]; 347 + } else if (ts.isIdentifier(target)) { 348 + return target; 349 + } else { 350 + return; 351 + } 352 + } 353 + }
+30 -41
packages/graphqlsp/src/ast/index.ts
··· 2 2 import { FragmentDefinitionNode, parse } from 'graphql'; 3 3 import * as checks from './checks'; 4 4 import { resolveTadaFragmentArray } from './resolve'; 5 + import { 6 + getDeclarationOfIdentifier, 7 + getValueOfIdentifier, 8 + getIdentifierOfChainExpression, 9 + } from './declaration'; 5 10 6 11 export { getSchemaName } from './checks'; 7 12 ··· 54 59 info: ts.server.PluginCreateInfo, 55 60 checker: ts.TypeChecker | undefined 56 61 ): checks.GraphQLCallNode | null { 57 - let prevElement: ts.Node | undefined; 58 - let element: ts.Node | undefined = input; 59 - // NOTE: Under certain circumstances, resolving an identifier can loop 60 - while (ts.isIdentifier(element) && element !== prevElement) { 61 - prevElement = element; 62 + if (!checker) return null; 62 63 63 - const definitions = info.languageService.getDefinitionAtPosition( 64 - element.getSourceFile().fileName, 65 - element.getStart() 66 - ); 67 - 68 - const fragment = definitions && definitions[0]; 69 - const externalSource = fragment && getSource(info, fragment.fileName); 70 - if (!fragment || !externalSource) return null; 71 - 72 - element = findNode(externalSource, fragment.textSpan.start); 73 - if (!element) return null; 74 - 75 - while (ts.isPropertyAccessExpression(element.parent)) 76 - element = element.parent; 64 + const value = getValueOfIdentifier(input, checker); 65 + if (!value) return null; 77 66 78 - if ( 79 - ts.isVariableDeclaration(element.parent) && 80 - element.parent.initializer && 81 - ts.isCallExpression(element.parent.initializer) 82 - ) { 83 - element = element.parent.initializer; 84 - } else if (ts.isPropertyAssignment(element.parent)) { 85 - element = element.parent.initializer; 86 - } else if (ts.isBinaryExpression(element.parent)) { 87 - element = ts.isPropertyAccessExpression(element.parent.right) 88 - ? element.parent.right.name 89 - : element.parent.right; 90 - } 91 - // If we find another Identifier, we continue resolving it 92 - } 93 - // Check whether we've got a `graphql()` or `gql()` call, by the 94 - // call expression's identifier 95 - return checks.isGraphQLCall(element, checker) ? element : null; 67 + // Check whether we've got a `graphql()` or `gql()` call 68 + return checks.isGraphQLCall(value, checker) ? value : null; 96 69 } 97 70 98 71 function unrollFragment( ··· 262 235 return fragments; 263 236 } 264 237 265 - const definitions = info.languageService.getDefinitionAtPosition( 266 - fileName, 267 - node.expression.getStart() 268 - ); 238 + if (!typeChecker) return fragments; 239 + 240 + const identifier = getIdentifierOfChainExpression(node.expression); 241 + if (!identifier) return fragments; 242 + 243 + const declaration = getDeclarationOfIdentifier(identifier, typeChecker); 244 + if (!declaration) return fragments; 245 + 246 + const sourceFile = declaration.getSourceFile(); 247 + if (!sourceFile) return fragments; 248 + 249 + const definitions = [ 250 + { 251 + fileName: sourceFile.fileName, 252 + textSpan: { 253 + start: declaration.getStart(), 254 + length: declaration.getWidth(), 255 + }, 256 + }, 257 + ]; 269 258 if (!definitions || !definitions.length) return fragments; 270 259 271 260 const def = definitions[0];
+27 -35
packages/graphqlsp/src/ast/resolve.ts
··· 1 1 import { print } from '@0no-co/graphql.web'; 2 2 import { ts } from '../ts'; 3 - import { findNode } from '.'; 4 - import { getSource } from '../ast'; 3 + import { 4 + getDeclarationOfIdentifier, 5 + getValueOfValueDeclaration, 6 + } from './declaration'; 5 7 6 8 type TemplateResult = { 7 9 combinedText: string; ··· 34 36 const resolvedSpans = node.template.templateSpans 35 37 .map(span => { 36 38 if (ts.isIdentifier(span.expression)) { 37 - const definitions = info.languageService.getDefinitionAtPosition( 38 - filename, 39 - span.expression.getStart() 39 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 40 + if (!typeChecker) return; 41 + 42 + const declaration = getDeclarationOfIdentifier( 43 + span.expression, 44 + typeChecker 40 45 ); 46 + if (!declaration) return; 41 47 42 - const def = definitions && definitions[0]; 43 - if (!def) return; 44 - 45 - const src = getSource(info, def.fileName); 46 - if (!src) return; 47 - 48 - const node = findNode(src, def.textSpan.start); 49 - if (!node || !node.parent) return; 50 - 51 - const parent = node.parent; 48 + const parent = declaration; 52 49 if (ts.isVariableDeclaration(parent)) { 53 50 const identifierName = span.expression.escapedText; 51 + const value = getValueOfValueDeclaration(parent); 52 + if (!value) return; 53 + 54 54 // we reduce by two to account for the "${" 55 55 const originalStart = span.expression.getStart() - 2; 56 56 const originalRange = { ··· 58 58 // we add 1 to account for the "}" 59 59 length: span.expression.end - originalStart + 1, 60 60 }; 61 - if ( 62 - parent.initializer && 63 - ts.isTaggedTemplateExpression(parent.initializer) 64 - ) { 61 + 62 + if (ts.isTaggedTemplateExpression(value)) { 65 63 const text = resolveTemplate( 66 - parent.initializer, 67 - def.fileName, 64 + value, 65 + parent.getSourceFile().fileName, 68 66 info 69 67 ); 70 68 templateText = templateText.replace( ··· 84 82 addedCharacters += text.combinedText.length - originalRange.length; 85 83 return alteredSpan; 86 84 } else if ( 87 - parent.initializer && 88 - ts.isAsExpression(parent.initializer) && 89 - ts.isTaggedTemplateExpression(parent.initializer.expression) 85 + ts.isAsExpression(value) && 86 + ts.isTaggedTemplateExpression(value.expression) 90 87 ) { 91 88 const text = resolveTemplate( 92 - parent.initializer.expression, 93 - def.fileName, 89 + value.expression, 90 + parent.getSourceFile().fileName, 94 91 info 95 92 ); 96 93 templateText = templateText.replace( ··· 109 106 addedCharacters += text.combinedText.length - originalRange.length; 110 107 return alteredSpan; 111 108 } else if ( 112 - parent.initializer && 113 - ts.isAsExpression(parent.initializer) && 114 - ts.isAsExpression(parent.initializer.expression) && 115 - ts.isObjectLiteralExpression( 116 - parent.initializer.expression.expression 117 - ) 109 + ts.isAsExpression(value) && 110 + ts.isAsExpression(value.expression) && 111 + ts.isObjectLiteralExpression(value.expression.expression) 118 112 ) { 119 - const astObject = JSON.parse( 120 - parent.initializer.expression.expression.getText() 121 - ); 113 + const astObject = JSON.parse(value.expression.expression.getText()); 122 114 const resolvedTemplate = print(astObject); 123 115 templateText = templateText.replace( 124 116 '${' + span.expression.escapedText + '}',
+26 -27
packages/graphqlsp/src/checkImports.ts
··· 1 1 import { ts } from './ts'; 2 2 import { FragmentDefinitionNode, Kind, parse } from 'graphql'; 3 3 4 - import { findAllCallExpressions, findAllImports, getSource } from './ast'; 4 + import { findAllCallExpressions, findAllImports } from './ast'; 5 5 import { resolveTemplate } from './ast/resolve'; 6 - import { 7 - VariableDeclaration, 8 - VariableStatement, 9 - isSourceFile, 10 - } from 'typescript'; 6 + import { getDeclarationOfIdentifier } from './ast/declaration'; 11 7 12 8 export const MISSING_FRAGMENT_CODE = 52003; 13 9 ··· 33 29 if (!imp.importClause) return; 34 30 35 31 if (imp.importClause.name) { 36 - const definitions = info.languageService.getDefinitionAtPosition( 37 - source.fileName, 38 - imp.importClause.name.getStart() 32 + const declaration = getDeclarationOfIdentifier( 33 + imp.importClause.name, 34 + typeChecker 39 35 ); 40 - const def = definitions && definitions[0]; 41 - if (def) { 42 - if (def.fileName.includes('node_modules')) return; 36 + if (declaration) { 37 + const sourceFile = declaration.getSourceFile(); 38 + if (sourceFile.fileName.includes('node_modules')) return; 43 39 44 - const externalSource = getSource(info, def.fileName); 40 + const externalSource = sourceFile; 45 41 if (!externalSource) return; 46 42 47 43 const fragmentsForImport = getFragmentsInSource( ··· 69 65 imp.importClause.namedBindings && 70 66 ts.isNamespaceImport(imp.importClause.namedBindings) 71 67 ) { 72 - const definitions = info.languageService.getDefinitionAtPosition( 73 - source.fileName, 74 - imp.importClause.namedBindings.getStart() 68 + const declaration = getDeclarationOfIdentifier( 69 + imp.importClause.namedBindings.name, 70 + typeChecker 75 71 ); 76 - const def = definitions && definitions[0]; 77 - if (def) { 78 - if (def.fileName.includes('node_modules')) return; 72 + if (declaration) { 73 + const sourceFile = declaration.getSourceFile(); 74 + if (sourceFile.fileName.includes('node_modules')) return; 79 75 80 - const externalSource = getSource(info, def.fileName); 76 + const externalSource = sourceFile; 81 77 if (!externalSource) return; 82 78 83 79 const fragmentsForImport = getFragmentsInSource( ··· 103 99 ts.isNamedImportBindings(imp.importClause.namedBindings) 104 100 ) { 105 101 imp.importClause.namedBindings.elements.forEach(el => { 106 - const definitions = info.languageService.getDefinitionAtPosition( 107 - source.fileName, 108 - el.getStart() 102 + const identifier = el.name || el.propertyName; 103 + if (!identifier) return; 104 + 105 + const declaration = getDeclarationOfIdentifier( 106 + identifier, 107 + typeChecker 109 108 ); 110 - const def = definitions && definitions[0]; 111 - if (def) { 112 - if (def.fileName.includes('node_modules')) return; 109 + if (declaration) { 110 + const sourceFile = declaration.getSourceFile(); 111 + if (sourceFile.fileName.includes('node_modules')) return; 113 112 114 - const externalSource = getSource(info, def.fileName); 113 + const externalSource = sourceFile; 115 114 if (!externalSource) return; 116 115 117 116 const fragmentsForImport = getFragmentsInSource(
-1
packages/graphqlsp/src/diagnostics.ts
··· 2 2 import { Diagnostic, getDiagnostics } from 'graphql-language-service'; 3 3 import { 4 4 FragmentDefinitionNode, 5 - GraphQLSchema, 6 5 Kind, 7 6 OperationDefinitionNode, 8 7 parse,
+8 -9
packages/graphqlsp/src/fieldUsage.ts
··· 2 2 import { parse, visit } from 'graphql'; 3 3 4 4 import { findNode } from './ast'; 5 - import { PropertyAccessExpression } from 'typescript'; 5 + import { getValueOfIdentifier } from './ast/declaration'; 6 6 7 7 export const UNUSED_FIELD_CODE = 52005; 8 8 ··· 154 154 // TODO: Scope utilities in checkFieldUsageInFile to deduplicate 155 155 const checker = info.languageService.getProgram()!.getTypeChecker(); 156 156 157 - const declaration = checker.getSymbolAtLocation(func)?.valueDeclaration; 158 - if (declaration && ts.isFunctionDeclaration(declaration)) { 159 - func = declaration; 160 - } else if ( 161 - declaration && 162 - ts.isVariableDeclaration(declaration) && 163 - declaration.initializer 157 + const value = getValueOfIdentifier(func, checker); 158 + if ( 159 + value && 160 + (ts.isFunctionDeclaration(value) || 161 + ts.isFunctionExpression(value) || 162 + ts.isArrowFunction(value)) 164 163 ) { 165 - func = declaration.initializer; 164 + func = value; 166 165 } 167 166 } 168 167
+32 -55
packages/graphqlsp/src/persisted.ts
··· 17 17 print, 18 18 visit, 19 19 } from '@0no-co/graphql.web'; 20 + import { 21 + getDeclarationOfIdentifier, 22 + getValueOfIdentifier, 23 + } from './ast/declaration'; 20 24 21 25 type PersistedAction = { 22 26 span: { ··· 278 282 filename: string, 279 283 info: ts.server.PluginCreateInfo 280 284 ): { node: ts.CallExpression | null; filename: string } => { 281 - // We look for the references of the generic so that we can use the document 282 - // to generate the hash. 283 - const references = info.languageService.getReferencesAtPosition( 284 - filename, 285 - typeQuery.exprName.getStart() 286 - ); 285 + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 286 + if (!typeChecker) return { node: null, filename }; 287 287 288 - if (!references) return { node: null, filename }; 288 + // Handle EntityName (Identifier | QualifiedName) 289 + let identifier: ts.Identifier | undefined; 290 + if (ts.isIdentifier(typeQuery.exprName)) { 291 + identifier = typeQuery.exprName; 292 + } else if (ts.isQualifiedName(typeQuery.exprName)) { 293 + // For qualified names like 'module.identifier', get the right-most identifier 294 + identifier = typeQuery.exprName.right; 295 + } 289 296 290 - const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 291 - let found: ts.CallExpression | null = null; 292 - let foundFilename = filename; 293 - references.forEach(ref => { 294 - if (found) return; 297 + if (!identifier) return { node: null, filename }; 295 298 296 - const source = getSource(info, ref.fileName); 297 - if (!source) return; 298 - const foundNode = findNode(source, ref.textSpan.start); 299 - if (!foundNode) return; 299 + const value = getValueOfIdentifier(identifier, typeChecker); 300 + if (!value || !checks.isGraphQLCall(value, typeChecker)) { 301 + return { node: null, filename }; 302 + } 300 303 301 - if ( 302 - ts.isVariableDeclaration(foundNode.parent) && 303 - foundNode.parent.initializer && 304 - checks.isGraphQLCall(foundNode.parent.initializer, typeChecker) 305 - ) { 306 - found = foundNode.parent.initializer; 307 - foundFilename = ref.fileName; 308 - } 309 - }); 310 - 311 - return { node: found, filename: foundFilename }; 304 + return { 305 + node: value as ts.CallExpression, 306 + filename: value.getSourceFile().fileName, 307 + }; 312 308 }; 313 309 314 310 export const getDocumentReferenceFromDocumentNode = ( ··· 317 313 info: ts.server.PluginCreateInfo 318 314 ): { node: ts.CallExpression | null; filename: string } => { 319 315 if (ts.isIdentifier(documentNodeArgument)) { 320 - // We look for the references of the generic so that we can use the document 321 - // to generate the hash. 322 - const references = info.languageService.getReferencesAtPosition( 323 - filename, 324 - documentNodeArgument.getStart() 325 - ); 326 - 327 - if (!references) return { node: null, filename }; 328 - 329 316 const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 330 - let found: ts.CallExpression | null = null; 331 - let foundFilename = filename; 332 - references.forEach(ref => { 333 - if (found) return; 317 + if (!typeChecker) return { node: null, filename }; 334 318 335 - const source = getSource(info, ref.fileName); 336 - if (!source) return; 337 - const foundNode = findNode(source, ref.textSpan.start); 338 - if (!foundNode) return; 339 - 340 - if ( 341 - ts.isVariableDeclaration(foundNode.parent) && 342 - foundNode.parent.initializer && 343 - checks.isGraphQLCall(foundNode.parent.initializer, typeChecker) 344 - ) { 345 - found = foundNode.parent.initializer; 346 - foundFilename = ref.fileName; 347 - } 348 - }); 319 + const value = getValueOfIdentifier(documentNodeArgument, typeChecker); 320 + if (!value || !checks.isGraphQLCall(value, typeChecker)) { 321 + return { node: null, filename }; 322 + } 349 323 350 - return { node: found, filename: foundFilename }; 324 + return { 325 + node: value as ts.CallExpression, 326 + filename: value.getSourceFile().fileName, 327 + }; 351 328 } else { 352 329 return { node: documentNodeArgument, filename }; 353 330 }
-1
packages/graphqlsp/src/quickInfo.ts
··· 14 14 import { resolveTemplate } from './ast/resolve'; 15 15 import { getToken } from './ast/token'; 16 16 import { Cursor } from './ast/cursor'; 17 - import { templates } from './ast/templates'; 18 17 import { SchemaRef } from './graphql/getSchema'; 19 18 20 19 export function getGraphQLQuickInfo(