···11+---
22+'@0no-co/graphqlsp': minor
33+---
44+55+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
···11+import { ts } from '../ts';
22+33+export type ValueDeclaration =
44+ | ts.BinaryExpression
55+ | ts.ArrowFunction
66+ | ts.BindingElement
77+ | ts.ClassDeclaration
88+ | ts.ClassExpression
99+ | ts.ClassStaticBlockDeclaration
1010+ | ts.ConstructorDeclaration
1111+ | ts.EnumDeclaration
1212+ | ts.EnumMember
1313+ | ts.ExportSpecifier
1414+ | ts.FunctionDeclaration
1515+ | ts.FunctionExpression
1616+ | ts.GetAccessorDeclaration
1717+ | ts.JsxAttribute
1818+ | ts.MethodDeclaration
1919+ | ts.ModuleDeclaration
2020+ | ts.ParameterDeclaration
2121+ | ts.PropertyAssignment
2222+ | ts.PropertyDeclaration
2323+ | ts.SetAccessorDeclaration
2424+ | ts.ShorthandPropertyAssignment
2525+ | ts.VariableDeclaration;
2626+2727+export type ValueOfDeclaration =
2828+ | ts.ClassExpression
2929+ | ts.ClassDeclaration
3030+ | ts.ArrowFunction
3131+ | ts.ClassStaticBlockDeclaration
3232+ | ts.ConstructorDeclaration
3333+ | ts.EnumDeclaration
3434+ | ts.FunctionDeclaration
3535+ | ts.GetAccessorDeclaration
3636+ | ts.SetAccessorDeclaration
3737+ | ts.MethodDeclaration
3838+ | ts.Expression;
3939+4040+/** Checks if a node is a `ts.Declaration` and a value.
4141+ * @remarks
4242+ * This checks if a given node is a value declaration only,
4343+ * excluding import/export specifiers, type declarations, and
4444+ * ambient declarations.
4545+ * All declarations that aren't JS(x) nodes will be discarded.
4646+ * This is based on `ts.isDeclarationKind`.
4747+ */
4848+export function isValueDeclaration(node: ts.Node): node is ValueDeclaration {
4949+ switch (node.kind) {
5050+ case ts.SyntaxKind.BinaryExpression:
5151+ case ts.SyntaxKind.ArrowFunction:
5252+ case ts.SyntaxKind.BindingElement:
5353+ case ts.SyntaxKind.ClassDeclaration:
5454+ case ts.SyntaxKind.ClassExpression:
5555+ case ts.SyntaxKind.ClassStaticBlockDeclaration:
5656+ case ts.SyntaxKind.Constructor:
5757+ case ts.SyntaxKind.EnumDeclaration:
5858+ case ts.SyntaxKind.EnumMember:
5959+ case ts.SyntaxKind.FunctionDeclaration:
6060+ case ts.SyntaxKind.FunctionExpression:
6161+ case ts.SyntaxKind.GetAccessor:
6262+ case ts.SyntaxKind.JsxAttribute:
6363+ case ts.SyntaxKind.MethodDeclaration:
6464+ case ts.SyntaxKind.Parameter:
6565+ case ts.SyntaxKind.PropertyAssignment:
6666+ case ts.SyntaxKind.PropertyDeclaration:
6767+ case ts.SyntaxKind.SetAccessor:
6868+ case ts.SyntaxKind.ShorthandPropertyAssignment:
6969+ case ts.SyntaxKind.VariableDeclaration:
7070+ return true;
7171+ default:
7272+ return false;
7373+ }
7474+}
7575+7676+/** Returns true if operator assigns a value unchanged */
7777+function isAssignmentOperator(token: ts.BinaryOperatorToken): boolean {
7878+ switch (token.kind) {
7979+ case ts.SyntaxKind.EqualsToken:
8080+ case ts.SyntaxKind.BarBarEqualsToken:
8181+ case ts.SyntaxKind.AmpersandAmpersandEqualsToken:
8282+ case ts.SyntaxKind.QuestionQuestionEqualsToken:
8383+ return true;
8484+ default:
8585+ return false;
8686+ }
8787+}
8888+8989+/** Evaluates to the declaration's value initializer or itself if it declares a value */
9090+export function getValueOfValueDeclaration(
9191+ node: ValueDeclaration
9292+): ValueOfDeclaration | undefined {
9393+ switch (node.kind) {
9494+ case ts.SyntaxKind.ClassExpression:
9595+ case ts.SyntaxKind.ClassDeclaration:
9696+ case ts.SyntaxKind.ArrowFunction:
9797+ case ts.SyntaxKind.ClassStaticBlockDeclaration:
9898+ case ts.SyntaxKind.Constructor:
9999+ case ts.SyntaxKind.EnumDeclaration:
100100+ case ts.SyntaxKind.FunctionDeclaration:
101101+ case ts.SyntaxKind.FunctionExpression:
102102+ case ts.SyntaxKind.GetAccessor:
103103+ case ts.SyntaxKind.SetAccessor:
104104+ case ts.SyntaxKind.MethodDeclaration:
105105+ return node;
106106+ case ts.SyntaxKind.BindingElement:
107107+ case ts.SyntaxKind.EnumMember:
108108+ case ts.SyntaxKind.JsxAttribute:
109109+ case ts.SyntaxKind.Parameter:
110110+ case ts.SyntaxKind.PropertyAssignment:
111111+ case ts.SyntaxKind.PropertyDeclaration:
112112+ case ts.SyntaxKind.VariableDeclaration:
113113+ return node.initializer;
114114+ case ts.SyntaxKind.BinaryExpression:
115115+ return isAssignmentOperator(node.operatorToken) ? node.right : undefined;
116116+ case ts.SyntaxKind.ShorthandPropertyAssignment:
117117+ return node.objectAssignmentInitializer;
118118+ default:
119119+ return undefined;
120120+ }
121121+}
122122+123123+// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L652-L654
124124+function climbPastPropertyOrElementAccess(node: ts.Node): ts.Node {
125125+ if (
126126+ node.parent &&
127127+ ts.isPropertyAccessExpression(node.parent) &&
128128+ node.parent.name === node
129129+ ) {
130130+ return node.parent;
131131+ } else if (
132132+ node.parent &&
133133+ ts.isElementAccessExpression(node.parent) &&
134134+ node.parent.argumentExpression === node
135135+ ) {
136136+ return node.parent;
137137+ } else {
138138+ return node;
139139+ }
140140+}
141141+142142+// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L602-L605
143143+function isNewExpressionTarget(node: ts.Node): node is ts.NewExpression {
144144+ const target = climbPastPropertyOrElementAccess(node).parent;
145145+ return ts.isNewExpression(target) && target.expression === node;
146146+}
147147+148148+// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L607-L610
149149+function isCallOrNewExpressionTarget(
150150+ node: ts.Node
151151+): node is ts.CallExpression | ts.NewExpression {
152152+ const target = climbPastPropertyOrElementAccess(node).parent;
153153+ return ts.isCallOrNewExpression(target) && target.expression === node;
154154+}
155155+156156+// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L716-L719
157157+function isNameOfFunctionDeclaration(node: ts.Node): boolean {
158158+ return (
159159+ ts.isIdentifier(node) &&
160160+ node.parent &&
161161+ ts.isFunctionLike(node.parent) &&
162162+ node.parent.name === node
163163+ );
164164+}
165165+166166+// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L2441-L2447
167167+function getNameFromPropertyName(name: ts.PropertyName): string | undefined {
168168+ if (ts.isComputedPropertyName(name)) {
169169+ return ts.isStringLiteralLike(name.expression) ||
170170+ ts.isNumericLiteral(name.expression)
171171+ ? name.expression.text
172172+ : undefined;
173173+ } else if (ts.isPrivateIdentifier(name) || ts.isMemberName(name)) {
174174+ return ts.idText(name);
175175+ } else {
176176+ return name.text;
177177+ }
178178+}
179179+180180+/** Resolves the declaration of an identifier.
181181+ * @remarks
182182+ * This returns the declaration node first found for an identifier by resolving an identifier's
183183+ * symbol via the type checker.
184184+ * @privateRemarks
185185+ * This mirrors the implementation of `getDefinitionAtPosition` in TS' language service. However,
186186+ * it removes all cases that aren't applicable to identifiers and removes the intermediary positional
187187+ * data structure, instead returning raw AST nodes.
188188+ */
189189+export function getDeclarationOfIdentifier(
190190+ node: ts.Identifier,
191191+ checker: ts.TypeChecker
192192+): ValueDeclaration | undefined {
193193+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L523-L540
194194+ let symbol = checker.getSymbolAtLocation(node);
195195+ if (
196196+ symbol?.declarations?.[0] &&
197197+ symbol.flags & ts.SymbolFlags.Alias &&
198198+ (node.parent === symbol?.declarations?.[0] ||
199199+ !ts.isNamespaceImport(symbol.declarations[0]))
200200+ ) {
201201+ // Resolve alias symbols, excluding self-referential symbols
202202+ const aliased = checker.getAliasedSymbol(symbol);
203203+ if (aliased.declarations) symbol = aliased;
204204+ }
205205+206206+ if (symbol && ts.isShorthandPropertyAssignment(node.parent)) {
207207+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L248-L257
208208+ // Resolve shorthand property assignments
209209+ const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(
210210+ symbol.valueDeclaration
211211+ );
212212+ if (shorthandSymbol) symbol = shorthandSymbol;
213213+ } else if (
214214+ ts.isBindingElement(node.parent) &&
215215+ ts.isObjectBindingPattern(node.parent.parent) &&
216216+ node === (node.parent.propertyName || node.parent.name)
217217+ ) {
218218+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L259-L280
219219+ // Resolve symbol of property in shorthand assignments
220220+ const name = getNameFromPropertyName(node);
221221+ const prop = name
222222+ ? checker.getTypeAtLocation(node.parent.parent).getProperty(name)
223223+ : undefined;
224224+ if (prop) symbol = prop;
225225+ } else if (
226226+ ts.isObjectLiteralElement(node.parent) &&
227227+ (ts.isObjectLiteralExpression(node.parent.parent) ||
228228+ ts.isJsxAttributes(node.parent.parent)) &&
229229+ node.parent.name === node
230230+ ) {
231231+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L298-L316
232232+ // Resolve symbol of property in object literal destructre expressions
233233+ const name = getNameFromPropertyName(node);
234234+ const prop = name
235235+ ? checker.getContextualType(node.parent.parent)?.getProperty(name)
236236+ : undefined;
237237+ if (prop) symbol = prop;
238238+ }
239239+240240+ if (symbol && symbol.declarations?.length) {
241241+ if (
242242+ symbol.flags & ts.SymbolFlags.Class &&
243243+ !(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) &&
244244+ isNewExpressionTarget(node)
245245+ ) {
246246+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L603-L610
247247+ // Resolve first class-like declaration for new expressions
248248+ for (const declaration of symbol.declarations) {
249249+ if (ts.isClassLike(declaration)) return declaration;
250250+ }
251251+ } else if (
252252+ isCallOrNewExpressionTarget(node) ||
253253+ isNameOfFunctionDeclaration(node)
254254+ ) {
255255+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L612-L616
256256+ // Resolve first function-like declaration for call expressions or named functions
257257+ for (const declaration of symbol.declarations) {
258258+ if (
259259+ ts.isFunctionLike(declaration) &&
260260+ !!(declaration as ts.FunctionLikeDeclaration).body &&
261261+ isValueDeclaration(declaration)
262262+ ) {
263263+ return declaration;
264264+ }
265265+ }
266266+ }
267267+268268+ // Account for assignments to property access expressions
269269+ // This resolves property access expressions to binding element parents
270270+ if (
271271+ symbol.valueDeclaration &&
272272+ ts.isPropertyAccessExpression(symbol.valueDeclaration)
273273+ ) {
274274+ const parent = symbol.valueDeclaration.parent;
275275+ if (
276276+ parent &&
277277+ ts.isBinaryExpression(parent) &&
278278+ parent.left === symbol.valueDeclaration
279279+ ) {
280280+ return parent;
281281+ }
282282+ }
283283+284284+ if (
285285+ symbol.valueDeclaration &&
286286+ isValueDeclaration(symbol.valueDeclaration)
287287+ ) {
288288+ // NOTE: We prefer value declarations, since the checker may have already applied conditions
289289+ // similar to `isValueDeclaration` and selected it beforehand
290290+ // Only use value declarations if they're not type/ambient declarations or imports/exports
291291+ return symbol.valueDeclaration;
292292+ }
293293+294294+ // Selecting the first available result, if any
295295+ // NOTE: We left out `!isExpandoDeclaration` as a condition, since `valueDeclaration` above
296296+ // should handle some of these cases, and we don't have to care about this subtlety as much for identifiers
297297+ // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L582-L590
298298+ for (const declaration of symbol.declarations) {
299299+ // Only use declarations if they're not type/ambient declarations or imports/exports
300300+ if (isValueDeclaration(declaration)) return declaration;
301301+ }
302302+ }
303303+304304+ return undefined;
305305+}
306306+307307+/** Loops {@link getDeclarationOfIdentifier} until a value of the identifier is found */
308308+export function getValueOfIdentifier(
309309+ node: ts.Identifier,
310310+ checker: ts.TypeChecker
311311+): ValueOfDeclaration | undefined {
312312+ while (ts.isIdentifier(node)) {
313313+ const declaration = getDeclarationOfIdentifier(node, checker);
314314+ if (!declaration) {
315315+ return undefined;
316316+ } else {
317317+ const value = getValueOfValueDeclaration(declaration);
318318+ if (value && ts.isIdentifier(value) && value !== node) {
319319+ // If the resolved value is another identifiers, we continue searching, if the
320320+ // identifier isn't self-referential
321321+ node = value;
322322+ } else {
323323+ return value;
324324+ }
325325+ }
326326+ }
327327+}
328328+329329+/** Resolves exressions that might not influence the target identifier */
330330+export function getIdentifierOfChainExpression(
331331+ node: ts.Expression
332332+): ts.Identifier | undefined {
333333+ let target: ts.Expression | undefined = node;
334334+ while (target) {
335335+ if (ts.isPropertyAccessExpression(target)) {
336336+ target = target.name;
337337+ } else if (
338338+ ts.isAsExpression(target) ||
339339+ ts.isSatisfiesExpression(target) ||
340340+ ts.isNonNullExpression(target) ||
341341+ ts.isParenthesizedExpression(target) ||
342342+ ts.isExpressionWithTypeArguments(target)
343343+ ) {
344344+ target = target.expression;
345345+ } else if (ts.isCommaListExpression(target)) {
346346+ target = target.elements[target.elements.length - 1];
347347+ } else if (ts.isIdentifier(target)) {
348348+ return target;
349349+ } else {
350350+ return;
351351+ }
352352+ }
353353+}
+30-41
packages/graphqlsp/src/ast/index.ts
···22import { FragmentDefinitionNode, parse } from 'graphql';
33import * as checks from './checks';
44import { resolveTadaFragmentArray } from './resolve';
55+import {
66+ getDeclarationOfIdentifier,
77+ getValueOfIdentifier,
88+ getIdentifierOfChainExpression,
99+} from './declaration';
510611export { getSchemaName } from './checks';
712···5459 info: ts.server.PluginCreateInfo,
5560 checker: ts.TypeChecker | undefined
5661): checks.GraphQLCallNode | null {
5757- let prevElement: ts.Node | undefined;
5858- let element: ts.Node | undefined = input;
5959- // NOTE: Under certain circumstances, resolving an identifier can loop
6060- while (ts.isIdentifier(element) && element !== prevElement) {
6161- prevElement = element;
6262+ if (!checker) return null;
62636363- const definitions = info.languageService.getDefinitionAtPosition(
6464- element.getSourceFile().fileName,
6565- element.getStart()
6666- );
6767-6868- const fragment = definitions && definitions[0];
6969- const externalSource = fragment && getSource(info, fragment.fileName);
7070- if (!fragment || !externalSource) return null;
7171-7272- element = findNode(externalSource, fragment.textSpan.start);
7373- if (!element) return null;
7474-7575- while (ts.isPropertyAccessExpression(element.parent))
7676- element = element.parent;
6464+ const value = getValueOfIdentifier(input, checker);
6565+ if (!value) return null;
77667878- if (
7979- ts.isVariableDeclaration(element.parent) &&
8080- element.parent.initializer &&
8181- ts.isCallExpression(element.parent.initializer)
8282- ) {
8383- element = element.parent.initializer;
8484- } else if (ts.isPropertyAssignment(element.parent)) {
8585- element = element.parent.initializer;
8686- } else if (ts.isBinaryExpression(element.parent)) {
8787- element = ts.isPropertyAccessExpression(element.parent.right)
8888- ? element.parent.right.name
8989- : element.parent.right;
9090- }
9191- // If we find another Identifier, we continue resolving it
9292- }
9393- // Check whether we've got a `graphql()` or `gql()` call, by the
9494- // call expression's identifier
9595- return checks.isGraphQLCall(element, checker) ? element : null;
6767+ // Check whether we've got a `graphql()` or `gql()` call
6868+ return checks.isGraphQLCall(value, checker) ? value : null;
9669}
97709871function unrollFragment(
···262235 return fragments;
263236 }
264237265265- const definitions = info.languageService.getDefinitionAtPosition(
266266- fileName,
267267- node.expression.getStart()
268268- );
238238+ if (!typeChecker) return fragments;
239239+240240+ const identifier = getIdentifierOfChainExpression(node.expression);
241241+ if (!identifier) return fragments;
242242+243243+ const declaration = getDeclarationOfIdentifier(identifier, typeChecker);
244244+ if (!declaration) return fragments;
245245+246246+ const sourceFile = declaration.getSourceFile();
247247+ if (!sourceFile) return fragments;
248248+249249+ const definitions = [
250250+ {
251251+ fileName: sourceFile.fileName,
252252+ textSpan: {
253253+ start: declaration.getStart(),
254254+ length: declaration.getWidth(),
255255+ },
256256+ },
257257+ ];
269258 if (!definitions || !definitions.length) return fragments;
270259271260 const def = definitions[0];