@plexcord-companion/ast-parser 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,450 @@
1
+ import { Format } from "@sadan4/devtools-pretty-printer";
2
+ import { collectVariableUsage, type VariableInfo } from "ts-api-utils";
3
+ import {
4
+ createSourceFile,
5
+ type Expression,
6
+ type Identifier,
7
+ isFunctionLike,
8
+ isLeftHandSideExpression,
9
+ isPropertyAccessExpression,
10
+ isVariableDeclaration,
11
+ isVariableDeclarationList,
12
+ type LeftHandSideExpression,
13
+ type MemberName,
14
+ type Node,
15
+ type PropertyAccessExpression,
16
+ type ReadonlyTextRange,
17
+ ScriptKind,
18
+ ScriptTarget,
19
+ type SourceFile,
20
+ SyntaxKind,
21
+ } from "typescript";
22
+
23
+ import { Cache, CacheGetter } from "@plexcord-companion/shared/decorators";
24
+ import { type Logger, NoopLogger } from "@plexcord-companion/shared/Logger";
25
+ import { type IPosition, Position } from "@plexcord-companion/shared/Position";
26
+ import { Range } from "@plexcord-companion/shared/Range";
27
+
28
+ import type { StringifiedModule } from "./StringifiedModule";
29
+ import type { Functionish } from "./types";
30
+ import { CharCode, findParent, getTokenAtPosition, isAssignmentExpression, isEOL } from "./util";
31
+
32
+ let logger: Logger = NoopLogger;
33
+
34
+ export function setLogger(newLogger: Logger): void {
35
+ logger = newLogger;
36
+ }
37
+
38
+ export class AstParser {
39
+ public static withFormattedText(text: string): AstParser {
40
+ return new this(Format(text));
41
+ }
42
+
43
+ public readonly text: string;
44
+
45
+ /**
46
+ * @CacheGetter
47
+ */
48
+ @CacheGetter()
49
+ public get sourceFile(): SourceFile {
50
+ return this.createSourceFile();
51
+ }
52
+
53
+ /**
54
+ * All the variables in the source file
55
+ * @CacheGetter
56
+ */
57
+ @CacheGetter()
58
+ public get vars(): Map<Identifier, VariableInfo> {
59
+ return collectVariableUsage(this.sourceFile);
60
+ }
61
+
62
+ /**
63
+ * @CacheGetter
64
+ */
65
+ @CacheGetter()
66
+ public get usesToVars(): Map<Identifier, VariableInfo> {
67
+ const map = new Map<Identifier, VariableInfo>();
68
+
69
+ for (const [, info] of this.vars) {
70
+ for (const { location } of info.uses) {
71
+ map.set(location, info);
72
+ }
73
+ // for (const decl of info.declarations) {
74
+ // map.set(decl, info);
75
+ // }
76
+ }
77
+
78
+ return map;
79
+ }
80
+
81
+ public getVarInfoFromUse(ident: Identifier): VariableInfo | undefined {
82
+ return this.usesToVars.get(ident);
83
+ }
84
+
85
+ // FIXME: add tests for this
86
+ /**
87
+ * @param use a use of a variable
88
+ * @param decl a declaration of a variable
89
+ * @returns true of the use is a use of the declaration, false otherwise
90
+ */
91
+ public isUseOf(use: Identifier | undefined, decl: Identifier | undefined): boolean {
92
+ if (!decl || !use)
93
+ return false;
94
+
95
+ const varInfo = this.vars.get(decl);
96
+
97
+ if (!varInfo)
98
+ return false;
99
+
100
+ const varInfoFromUse = this.usesToVars.get(use);
101
+
102
+ return varInfoFromUse === varInfo;
103
+ }
104
+
105
+ public constructor(text: string) {
106
+ this.text = text;
107
+ }
108
+
109
+ /**
110
+ * given something like this
111
+ * ```js
112
+ * const bar = "foo";
113
+ * const baz = bar;
114
+ * const qux = baz;
115
+ * ```
116
+ * if given `qux` it will return `[bar, baz]`;
117
+ *
118
+ * fails on something where a variable is reassigned
119
+ */
120
+ public unwrapVariableDeclaration(ident: Identifier): Identifier[] | undefined {
121
+ const arr: Identifier[] = [];
122
+ let last = ident;
123
+
124
+ while (true) {
125
+ const [varDec, ...rest] = this.getVarInfoFromUse(last)?.declarations ?? [];
126
+
127
+ if (!varDec)
128
+ break;
129
+ if (rest.length) {
130
+ arr.length = 0;
131
+ break;
132
+ }
133
+ arr.push(last = varDec);
134
+ }
135
+ if (arr.length !== 0)
136
+ return arr;
137
+ logger.debug("[AstParser] Failed finding variable declaration");
138
+ }
139
+
140
+ /**
141
+ * Used for interop with other systems
142
+ */
143
+ // FIXME: PACKAGE -
144
+ public serialize(): StringifiedModule {
145
+ return {
146
+ content: this.text,
147
+ } satisfies StringifiedModule;
148
+ }
149
+
150
+ /**
151
+ * given the `x` of
152
+ * ```js
153
+ * const x = {
154
+ * foo: bar
155
+ * }
156
+ * ```
157
+ * NOTE: this must be the exact x, not a use of it
158
+ * @returns the expression {foo: bar}
159
+ */
160
+ public getVariableInitializer(ident: Identifier): Expression | undefined {
161
+ const dec = ident.parent;
162
+
163
+ if (!isVariableDeclaration(dec))
164
+ return;
165
+ return dec.initializer;
166
+ }
167
+
168
+ /**
169
+ * TODO: document this
170
+ */
171
+ public isConstDeclared(info: VariableInfo): [Identifier] | false {
172
+ const len = info.declarations.length;
173
+
174
+ if (len !== 1) {
175
+ if (len > 1) {
176
+ logger.warn("[AstParser] isConstDeclared: ?????");
177
+ }
178
+ return false;
179
+ }
180
+
181
+ const [decl] = info.declarations;
182
+ const varDecl = findParent(decl, isVariableDeclarationList);
183
+
184
+ return ((varDecl?.flags ?? 0) & SyntaxKind.ConstKeyword) !== 0 ? [decl] : false;
185
+ }
186
+
187
+ // TODO: add tests for this
188
+ /**
189
+ * @param expr the property access expression to flatten
190
+ *
191
+ * given a property access expression like `foo.bar.baz.qux`
192
+ *
193
+ * @returns the identifiers [`foo`, `bar`, `baz`, `qux`]
194
+ *
195
+ * given another property access expression like `foo.bar.baz[0].qux.abc`
196
+ *
197
+ * @returns the elementAccessExpression, followed by the identifiers [`foo.bar.baz[0]`, `qux`, `abc`]
198
+ */
199
+ public flattenPropertyAccessExpression(expr: PropertyAccessExpression | undefined):
200
+ | readonly [LeftHandSideExpression, ...MemberName[]]
201
+ | undefined {
202
+ if (!expr)
203
+ return undefined;
204
+
205
+ const toRet = [] as any as [LeftHandSideExpression, ...MemberName[]];
206
+ let cur = expr;
207
+
208
+ do {
209
+ toRet.unshift(cur.name);
210
+ if (isLeftHandSideExpression(cur.expression) && !isPropertyAccessExpression(cur.expression)) {
211
+ toRet.unshift(cur.expression);
212
+ return toRet;
213
+ }
214
+ if (!isPropertyAccessExpression(cur.expression)) {
215
+ toRet.unshift(cur.expression);
216
+ return;
217
+ }
218
+ } while ((cur = cur.expression));
219
+ }
220
+
221
+ /**
222
+ * given a variable, if it has a single assignment in this file, return the expression assigned to it
223
+ *
224
+ * returns undefined if there are multiple assignments, or if the variable is assigned more than once
225
+ */
226
+ public findSingleAssignment(info: VariableInfo): Expression | undefined {
227
+ const { declarations, uses } = info;
228
+
229
+ if (declarations.length !== 1) {
230
+ logger.warn("[AstParser] findSingleAssignment: multiple declarations");
231
+ return;
232
+ }
233
+
234
+ const [decl] = declarations;
235
+
236
+ if (this.isConstDeclared(info)) {
237
+ const init = this.getVariableInitializer(decl);
238
+
239
+ if (!init) {
240
+ logger.warn("[AstParser] findSingleAssignment: const variable without initializer");
241
+ }
242
+ return init;
243
+ }
244
+
245
+ let init: Expression | undefined;
246
+
247
+ for (const { location } of uses) {
248
+ if (isAssignmentExpression(location.parent)) {
249
+ // filter out cases like `<some other thing> = location`
250
+ if (location.parent.left !== location) {
251
+ continue;
252
+ }
253
+ if (init || location.parent.operatorToken.kind !== SyntaxKind.EqualsToken) {
254
+ return;
255
+ }
256
+ init = location.parent.right;
257
+ }
258
+ }
259
+
260
+ return init;
261
+ }
262
+
263
+ /**
264
+ * Create the source file for this parser
265
+ *
266
+ * MUST SET PARENT NODES
267
+ * @Cache
268
+ */
269
+ @Cache()
270
+ protected createSourceFile(): SourceFile {
271
+ return createSourceFile(
272
+ "file.tsx",
273
+ this.text,
274
+ ScriptTarget.ESNext,
275
+ true,
276
+ ScriptKind.TSX,
277
+ );
278
+ }
279
+
280
+ /** Returns the token at or following the specified position or undefined if none is found inside `parent`. */
281
+ public getTokenAtOffset(pos: number): Node | undefined {
282
+ return getTokenAtPosition(this.sourceFile, pos, this.sourceFile, false);
283
+ }
284
+
285
+ public getTokenAtPosition(pos: IPosition): Node | undefined {
286
+ return this.getTokenAtOffset(this.offsetAt(pos));
287
+ }
288
+
289
+ /**
290
+ * convert two offsets to a range
291
+ *
292
+ * **DO NOT USE WITH AN AST NODE, IT WILL LEAD TO INCORRECT LOCATIONS**
293
+ * @see makeRangeFromAstNode
294
+ */
295
+ public makeRange({ pos, end }: ReadonlyTextRange): Range {
296
+ return new Range(this.positionAt(pos), this.positionAt(end));
297
+ }
298
+
299
+ public makeRangeFromAstNode(node: Node): Range {
300
+ return new Range(this.positionAt(node.getStart(this.sourceFile)), this.positionAt(node.end));
301
+ }
302
+
303
+ public makeRangeFromAnonFunction(func: Functionish): Range {
304
+ const { pos } = func.body ?? { pos: func.getEnd() };
305
+
306
+ return this.makeRange({
307
+ pos: func.getStart(),
308
+ end: pos,
309
+ });
310
+ }
311
+
312
+ public makeRangeFromFunctionDef(ident: Identifier): Range | undefined {
313
+ const { declarations } = this.getVarInfoFromUse(ident) ?? {};
314
+
315
+ if (!declarations) {
316
+ logger.debug("makeRangeFromFunctionDef: no declarations found for identifier");
317
+ return undefined;
318
+ }
319
+ if (declarations.length !== 1) {
320
+ logger.debug("makeRangeFromFunctionDef: zero or multiple declarations found for identifier");
321
+ return undefined;
322
+ }
323
+ if (declarations[0].parent && !isFunctionLike(declarations[0].parent)) {
324
+ logger.debug("makeRangeFromFunctionDef: dec. parent is not a function");
325
+ return undefined;
326
+ }
327
+ return this.makeRangeFromAstNode(declarations[0]);
328
+ }
329
+
330
+
331
+ /**
332
+ * Converts the position to a zero-based offset.
333
+ * Invalid positions are adjusted as described in {@link Position.line}
334
+ * and {@link Position.character}.
335
+ *
336
+ * @param position A position.
337
+ * @return A valid zero-based offset.
338
+ */
339
+ // copied from vscode-languageserver-node
340
+ public offsetAt(position: IPosition): number {
341
+ const { lineOffsets } = this;
342
+
343
+ if (position.line >= lineOffsets.length) {
344
+ return this.text.length;
345
+ } else if (position.line < 0) {
346
+ return 0;
347
+ }
348
+
349
+ const lineOffset = lineOffsets[position.line];
350
+
351
+ if (position.character <= 0) {
352
+ return lineOffset;
353
+ }
354
+
355
+ const nextLineOffset
356
+ = position.line + 1 < lineOffsets.length
357
+ ? lineOffsets[position.line + 1]
358
+ : this.text.length;
359
+
360
+ const offset = Math.min(lineOffset + position.character, nextLineOffset);
361
+
362
+ return this.ensureBeforeEOL(offset, lineOffset);
363
+ }
364
+
365
+ // methods copied from vscode-languageserver-node
366
+ /**
367
+ * @CacheGetter
368
+ */
369
+ @CacheGetter()
370
+ private get lineOffsets() {
371
+ return this.computeLineOffsets(true);
372
+ }
373
+
374
+ @CacheGetter()
375
+ /**
376
+ * @CacheGetter
377
+ */
378
+ public get lineCount(): number {
379
+ return this.lineOffsets.length;
380
+ }
381
+
382
+ private ensureBeforeEOL(offset: number, lineOffset: number): number {
383
+ while (offset > lineOffset && isEOL(this.text.charCodeAt(offset - 1))) {
384
+ offset--;
385
+ }
386
+ return offset;
387
+ }
388
+
389
+ private computeLineOffsets(isAtLineStart: boolean, textOffset = 0): number[] {
390
+ const { text } = this;
391
+ const result: number[] = isAtLineStart ? [textOffset] : [];
392
+
393
+ for (let i = 0; i < text.length; i++) {
394
+ const ch = text.charCodeAt(i);
395
+
396
+ if (isEOL(ch)) {
397
+ if (
398
+ ch === CharCode.CarriageReturn
399
+ && i + 1 < text.length
400
+ && text.charCodeAt(i + 1) === CharCode.LineFeed
401
+ ) {
402
+ i++;
403
+ }
404
+ result.push(textOffset + i + 1);
405
+ }
406
+ }
407
+ return result;
408
+ }
409
+
410
+ /**
411
+ * Converts a zero-based offset to a position.
412
+ *
413
+ * @param offset A zero-based offset.
414
+ * @return A valid {@link Position position}.
415
+ * @example The text document "ab\ncd" produces:
416
+ * position { line: 0, character: 0 } for `offset` 0.
417
+ * position { line: 0, character: 1 } for `offset` 1.
418
+ * position { line: 0, character: 2 } for `offset` 2.
419
+ * position { line: 1, character: 0 } for `offset` 3.
420
+ * position { line: 1, character: 1 } for `offset` 4.
421
+ */
422
+ public positionAt(offset: number): Position {
423
+ offset = Math.max(Math.min(offset, this.text.length), 0);
424
+
425
+ const { lineOffsets } = this;
426
+
427
+ let low = 0,
428
+ high = lineOffsets.length;
429
+
430
+ if (high === 0) {
431
+ return new Position(0, offset);
432
+ }
433
+ while (low < high) {
434
+ const mid = Math.floor((low + high) / 2);
435
+
436
+ if (lineOffsets[mid] > offset) {
437
+ high = mid;
438
+ } else {
439
+ low = mid + 1;
440
+ }
441
+ }
442
+
443
+ // low is the least x for which the line offset is larger than the current offset
444
+ // or array.length if no line offset is larger than the current offset
445
+ const line = low - 1;
446
+
447
+ offset = this.ensureBeforeEOL(offset, lineOffsets[line]);
448
+ return new Position(line, offset - lineOffsets[line]);
449
+ }
450
+ }
@@ -0,0 +1,3 @@
1
+ export interface StringifiedModule {
2
+ content: string;
3
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./AstParser";
2
+ export type * from "./types";
3
+ export * from "./util";
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { ArrowFunction, FunctionExpression, FunctionLikeDeclaration, Identifier, ModuleExportName, Node } from "typescript";
2
+
3
+ export type Functionish = FunctionLikeDeclaration | ArrowFunction | FunctionExpression;
4
+ export type AnyFunction = FunctionExpression | ArrowFunction;
5
+
6
+ export type AssertedType<
7
+ T extends Function,
8
+ E = any,
9
+ >
10
+ = T extends (a: any) => a is infer R ? R extends E ? R : never : never;
11
+
12
+ export type CBAssertion<U = undefined, N = never> = <
13
+ F extends (n: Node) => n is Node,
14
+ R extends Node = AssertedType<F, Node>,
15
+ >(
16
+ node: Node | N,
17
+ func: F extends (n: Node) => n is R ? F : never
18
+ ) => R | U;
19
+
20
+ export type Import = {
21
+ default: boolean;
22
+ source: string;
23
+ namespace: boolean;
24
+ orig?: ModuleExportName;
25
+ as: Identifier;
26
+ };
27
+
28
+ export type WithParent<N, P> = Omit<N, "parent"> & {
29
+ parent: P;
30
+ };
31
+