Skip to content

Commit c961c17

Browse files
kittenJoviDeCroock
andauthored
refactor(graphqlsp): Add declaration helpers to replace language services (#351)
Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>
1 parent 180702b commit c961c17

File tree

9 files changed

+481
-169
lines changed

9 files changed

+481
-169
lines changed

.changeset/puny-ghosts-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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.
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
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+
}

0 commit comments

Comments
 (0)