@vencord-companion/ast-parser 1.1.3 → 1.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vencord-companion/ast-parser",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "keywords": [],
@@ -11,7 +11,7 @@
11
11
  "license": "LGPL-3.0-or-later",
12
12
  "dependencies": {
13
13
  "@sadan4/devtools-pretty-printer": "^1.0.4",
14
- "@vencord-companion/shared": "1.1.3"
14
+ "@vencord-companion/shared": "1.2.1"
15
15
  },
16
16
  "exports": {
17
17
  "./*": "./dist/*.js",
@@ -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 "@vencord-companion/shared/decorators";
24
+ import { type Logger, NoopLogger } from "@vencord-companion/shared/Logger";
25
+ import { type IPosition, Position } from "@vencord-companion/shared/Position";
26
+ import { Range } from "@vencord-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
+
package/src/util.ts ADDED
@@ -0,0 +1,571 @@
1
+ import { isStaticKeyword } from "ts-api-utils";
2
+ import {
3
+ type AssignmentExpression,
4
+ type AssignmentOperatorToken,
5
+ type BinaryExpression,
6
+ type Block,
7
+ type DefaultKeyword,
8
+ forEachChild,
9
+ type Identifier,
10
+ type ImportClause,
11
+ isArrowFunction,
12
+ isBigIntLiteral,
13
+ isBinaryExpression,
14
+ isBlock,
15
+ isConstructorDeclaration,
16
+ isExpressionStatement,
17
+ isFunctionDeclaration,
18
+ isFunctionExpression,
19
+ isGetAccessorDeclaration,
20
+ isIdentifier,
21
+ isImportClause,
22
+ isImportDeclaration,
23
+ isImportSpecifier,
24
+ isJsxText,
25
+ isMethodDeclaration,
26
+ isNamespaceImport as _TS_isNamespaceImport,
27
+ isNumericLiteral,
28
+ isObjectLiteralExpression,
29
+ isPropertyAccessExpression,
30
+ isRegularExpressionLiteral,
31
+ isReturnStatement,
32
+ isSetAccessorDeclaration,
33
+ isStringLiteral,
34
+ isStringLiteralLike,
35
+ isTokenKind,
36
+ isVariableDeclaration,
37
+ type LiteralToken,
38
+ type NamespaceImport,
39
+ type Node,
40
+ type ObjectLiteralElementLike,
41
+ type ObjectLiteralExpression,
42
+ type PlusToken,
43
+ type PropertyAccessExpression,
44
+ type PropertyDeclaration,
45
+ type SourceFile,
46
+ SyntaxKind,
47
+ type SyntaxList,
48
+ type Token,
49
+ type VariableDeclaration,
50
+ } from "typescript";
51
+
52
+ import type { AnyFunction, AssertedType, CBAssertion, Functionish, Import, WithParent } from "./types";
53
+
54
+ export const enum CharCode {
55
+ /**
56
+ * The `\n` character.
57
+ */
58
+ LineFeed = 10,
59
+ /**
60
+ * The `\r` character.
61
+ */
62
+ CarriageReturn = 13,
63
+ }
64
+
65
+ export function isEOL(char: number): boolean {
66
+ return char === CharCode.CarriageReturn || char === CharCode.LineFeed;
67
+ }
68
+
69
+ /**
70
+ * given a function like this, returns the identifier for x
71
+ * @example function(){
72
+ * // any code here
73
+ * return x;
74
+ * }
75
+ * @param func a function to get the return value of
76
+ * @returns the return identifier, if any
77
+ */
78
+ export function findReturnIdentifier(func: Functionish): Identifier | undefined {
79
+ if (!func.body)
80
+ return undefined;
81
+ if (isBlock(func.body))
82
+ return _findReturnIdentifier(func.body);
83
+ if (isIdentifier(func.body))
84
+ return func.body;
85
+ }
86
+
87
+ function _findReturnIdentifier(func: Block): Identifier | undefined {
88
+ const lastStatement = func.statements.at(-1);
89
+
90
+ if (
91
+ !lastStatement
92
+ || !isReturnStatement(lastStatement)
93
+ || !lastStatement.expression
94
+ || !isIdentifier(lastStatement.expression)
95
+ )
96
+ return undefined;
97
+
98
+ return lastStatement.expression;
99
+ }
100
+
101
+ /**
102
+ * given an object literal, returns the property assignment for `prop` if it exists
103
+ *
104
+ * if prop is defined more than once, returns the first
105
+ * @example
106
+ * {
107
+ * exProp: "examplePropValue"
108
+ * }
109
+ * @param prop exProp
110
+ */
111
+ export function findObjectLiteralByKey(
112
+ object: ObjectLiteralExpression,
113
+ prop: string,
114
+ ): ObjectLiteralElementLike | undefined {
115
+ return object.properties.find((x) => x.name?.getText() === prop);
116
+ }
117
+
118
+ /**
119
+ * first parent
120
+ */
121
+ export const findParent: CBAssertion<undefined, undefined> = (node, func) => {
122
+ if (!node)
123
+ return undefined;
124
+ while (!func(node)) {
125
+ if (!node.parent)
126
+ return undefined;
127
+ node = node.parent;
128
+ }
129
+ return node;
130
+ };
131
+
132
+ export function findParentLimited<
133
+ F extends (n: Node) => n is Node,
134
+ R extends Node = AssertedType<F, Node>,
135
+ >(
136
+ node: Node,
137
+ func: F extends (n: Node) => n is R ? F : never,
138
+ limit: number,
139
+ ): R | undefined {
140
+ if (!node)
141
+ return undefined;
142
+ limit += 1;
143
+ while (limit-- && !func(node)) {
144
+ if (!node.parent)
145
+ return undefined;
146
+ node = node.parent;
147
+ }
148
+ if (limit < 0) {
149
+ return undefined;
150
+ }
151
+ return node as R;
152
+ }
153
+
154
+ // FIXME: try simplifying this
155
+ /**
156
+ * @param node the node to start from
157
+ * @param func a function to check if the parent matches
158
+ */
159
+ export const lastParent: CBAssertion<undefined, undefined> = (node, func) => {
160
+ if (!node)
161
+ return undefined;
162
+ if (!node.parent)
163
+ return undefined;
164
+ while (func(node.parent)) {
165
+ if (!node.parent)
166
+ break;
167
+ node = node.parent;
168
+ }
169
+ return func(node) ? node : undefined;
170
+ };
171
+
172
+ export const lastChild: CBAssertion<undefined> = (node, func) => {
173
+ if (!node)
174
+ return undefined;
175
+
176
+ const c = node.getChildren();
177
+
178
+ if (c.length === 0) {
179
+ if (func(node))
180
+ return node;
181
+ return undefined;
182
+ }
183
+ if (c.length === 1) {
184
+ if (func(c[0]))
185
+ return lastChild(c[0], func);
186
+ if (func(node))
187
+ return node;
188
+ return undefined;
189
+ }
190
+
191
+ const x = one(c, func);
192
+
193
+ if (x) {
194
+ return lastChild(x, func);
195
+ }
196
+ if (func(node))
197
+ return node;
198
+ return undefined;
199
+ };
200
+
201
+ // FIXME: this seems really stupid
202
+ export function one<
203
+ T,
204
+ F extends (t: T) => t is T,
205
+ R extends T = AssertedType<F, T>,
206
+ >(
207
+ arr: readonly T[],
208
+ func: F extends (t: T) => t is R ? F : never,
209
+ ): R | undefined {
210
+ const filter = arr.filter<R>(func);
211
+
212
+ return filter.length === 1 ? filter[0] : undefined;
213
+ }
214
+
215
+ export function isDefaultImport(x: Identifier): x is WithParent<typeof x, ImportClause> {
216
+ return isImportClause(x.parent);
217
+ }
218
+
219
+ /**
220
+ * @param node any identifier in an import statment
221
+ */
222
+ export function getImportName(node: Identifier): Pick<Import, "orig" | "as"> {
223
+ // default or namespace
224
+ if (isDefaultImport(node) || isNamespaceImport(node))
225
+ return { as: node };
226
+
227
+ const specifier = findParent(node, isImportSpecifier);
228
+
229
+ if (!specifier)
230
+ throw new Error("x is not in an import statment");
231
+ return {
232
+ orig: specifier.propertyName,
233
+ as: specifier.name,
234
+ };
235
+ }
236
+
237
+ // i fucking hate jsdoc
238
+ /**
239
+ * given an access chain like `one.b.three.d` \@*returns* — `[one?, b?]`
240
+ *
241
+ * if b is returned, one is gaurenteed to be defined
242
+ * @param node any node in the property access chain
243
+ */
244
+ export function getLeadingIdentifier(node: Node | undefined):
245
+ readonly [Identifier, undefined]
246
+ | readonly [Identifier, Identifier]
247
+ | readonly [undefined, undefined] {
248
+ if (!node)
249
+ return [node, undefined];
250
+
251
+ const { expression: module, name: wpExport } = (() => {
252
+ const lastP = lastParent(node, isPropertyAccessExpression);
253
+
254
+ return lastP && lastChild(lastP, isPropertyAccessExpression);
255
+ })() ?? {};
256
+
257
+ if (!module || !isIdentifier(module))
258
+ return [undefined, undefined];
259
+ return [
260
+ module,
261
+ wpExport ? isIdentifier(wpExport) ? wpExport : undefined : undefined,
262
+ ];
263
+ }
264
+
265
+ export function isInImportStatment(x: Node): boolean {
266
+ return findParent(x, isImportDeclaration) != null;
267
+ }
268
+
269
+ /**
270
+ * @param x an identifier in the import statment, not just any imported identifier
271
+ * @returns the source of the import statment
272
+ * @example
273
+ * ```
274
+ * import { x } from "source"
275
+ * ```
276
+ * @returns "source"
277
+ */
278
+ export function getImportSource(x: Identifier): string {
279
+ const clause = findParent(x, isImportDeclaration);
280
+
281
+ if (!clause)
282
+ throw new Error("x is not in an import statment");
283
+ // getText returns with quotes, but the prop text does not have them ????
284
+ return clause.moduleSpecifier.getText()
285
+ .slice(1, -1);
286
+ }
287
+
288
+ export function isNamespaceImport(x: Identifier): x is WithParent<typeof x, NamespaceImport> {
289
+ return _TS_isNamespaceImport(x.parent);
290
+ }
291
+
292
+ export function isDefaultKeyword(n: Node): n is DefaultKeyword {
293
+ return n.kind === SyntaxKind.DefaultKeyword;
294
+ }
295
+
296
+
297
+ export function isSyntaxList(node: Node): node is SyntaxList {
298
+ return node.kind === SyntaxKind.SyntaxList;
299
+ }
300
+
301
+ /**
302
+ * given a function like
303
+ * ```ts
304
+ * function myFunc() {
305
+ * // any code here
306
+ * return a.b; // can be anything else, eg a.b.c a.b[anything]
307
+ * }
308
+ * ```
309
+ * @returns the returned property access expression, if any
310
+ **/
311
+ export function findReturnPropertyAccessExpression(func: AnyFunction): PropertyAccessExpression | undefined {
312
+ if (isBlock(func.body))
313
+ return _findReturnPropertyAccessExpression(func.body);
314
+ if (isPropertyAccessExpression(func.body))
315
+ return func.body;
316
+ }
317
+
318
+ function _findReturnPropertyAccessExpression(func: Block): PropertyAccessExpression | undefined {
319
+ const lastStatment = func.statements.at(-1);
320
+
321
+ if (
322
+ !lastStatment
323
+ || !isReturnStatement(lastStatment)
324
+ || !lastStatment.expression
325
+ || !isPropertyAccessExpression(lastStatment.expression)
326
+ )
327
+ return undefined;
328
+
329
+ return lastStatment.expression;
330
+ }
331
+
332
+ export function tryParseStringOrNumberLiteral(node: Node | undefined): string | undefined {
333
+ if (!node)
334
+ return;
335
+ if (isStringLiteralLike(node) || isNumericLiteral(node)) {
336
+ return node.text;
337
+ }
338
+ }
339
+
340
+ export function isLiteralish(node: Node): node is LiteralToken {
341
+ return isStringLiteralLike(node)
342
+ || isNumericLiteral(node)
343
+ || isBigIntLiteral(node)
344
+ || isJsxText(node)
345
+ || isRegularExpressionLiteral(node);
346
+ }
347
+
348
+ export function isFunctionish(node: Node): node is Functionish {
349
+ return (
350
+ isFunctionDeclaration(node)
351
+ || isMethodDeclaration(node)
352
+ || isGetAccessorDeclaration(node)
353
+ || isSetAccessorDeclaration(node)
354
+ || isConstructorDeclaration(node)
355
+ || isFunctionExpression(node)
356
+ || isArrowFunction(node)
357
+
358
+ );
359
+ }
360
+
361
+ const DIRECTIVE_PREFIX = "use ";
362
+
363
+ /**
364
+ * returns if the node is a directive
365
+ * ```ts
366
+ * "use strict"; // true
367
+ * "use something"; // true
368
+ * "not a directive"; // false
369
+ * 42; // false
370
+ * ```
371
+ */
372
+ export function isDirective(node: Node): boolean {
373
+ if (!isExpressionStatement(node)) {
374
+ return false;
375
+ }
376
+
377
+ const { expression } = node;
378
+
379
+ if (!isStringLiteral(expression)) {
380
+ return false;
381
+ }
382
+
383
+ const { text } = expression;
384
+
385
+ if (text.length <= DIRECTIVE_PREFIX.length) {
386
+ return false;
387
+ }
388
+
389
+ return text.startsWith(DIRECTIVE_PREFIX);
390
+ }
391
+
392
+ const ASSIGNMENT_TOKENS: Partial<Record<SyntaxKind, true>> = {
393
+ [SyntaxKind.EqualsToken]: true,
394
+ [SyntaxKind.PlusEqualsToken]: true,
395
+ [SyntaxKind.MinusEqualsToken]: true,
396
+ [SyntaxKind.AsteriskAsteriskEqualsToken]: true,
397
+ [SyntaxKind.AsteriskEqualsToken]: true,
398
+ [SyntaxKind.SlashEqualsToken]: true,
399
+ [SyntaxKind.PercentEqualsToken]: true,
400
+ [SyntaxKind.AmpersandEqualsToken]: true,
401
+ [SyntaxKind.BarEqualsToken]: true,
402
+ [SyntaxKind.CaretEqualsToken]: true,
403
+ [SyntaxKind.LessThanLessThanEqualsToken]: true,
404
+ [SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: true,
405
+ [SyntaxKind.GreaterThanGreaterThanEqualsToken]: true,
406
+ [SyntaxKind.BarBarEqualsToken]: true,
407
+ [SyntaxKind.AmpersandAmpersandEqualsToken]: true,
408
+ [SyntaxKind.QuestionQuestionEqualsToken]: true,
409
+ };
410
+
411
+ export function isAssignmentExpression(node: Node | undefined):
412
+ node is AssignmentExpression<AssignmentOperatorToken> {
413
+ if (!node || !isBinaryExpression(node))
414
+ return false;
415
+
416
+ return ASSIGNMENT_TOKENS[node.operatorToken.kind] === true;
417
+ }
418
+
419
+ export function isVariableAssignmentLike(node: Node | undefined):
420
+ node is
421
+ | (
422
+ & Omit<VariableDeclaration, "name" | "initializer">
423
+ & {
424
+ name: Identifier;
425
+ initializer: Exclude<VariableDeclaration["initializer"], undefined>;
426
+ }
427
+ )
428
+ | (Omit<AssignmentExpression<AssignmentOperatorToken>, "left"> & { left: Identifier; }) {
429
+ if (!node)
430
+ return false;
431
+
432
+ if (isVariableDeclaration(node)) {
433
+ return isIdentifier(node.name) && !!node.initializer;
434
+ } else if (isBinaryExpression(node)) {
435
+ return isAssignmentExpression(node);
436
+ }
437
+ return false;
438
+ }
439
+
440
+ export function isBinaryPlusExpression(node: Node):
441
+ node is
442
+ & BinaryExpression
443
+ & {
444
+ readonly operatorToken: PlusToken;
445
+ } {
446
+ if (!isBinaryExpression(node)) {
447
+ return false;
448
+ }
449
+ if (node.operatorToken.kind !== SyntaxKind.PlusToken) {
450
+ return false;
451
+ }
452
+ return true;
453
+ }
454
+
455
+
456
+ /**
457
+ * @license MIT
458
+ * taken from tsutils, license below
459
+ * The MIT License (MIT)
460
+ *
461
+ * Copyright (c) 2017 Klaus Meinhardt
462
+ *
463
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
464
+ * of this software and associated documentation files (the "Software"), to deal
465
+ * in the Software without restriction, including without limitation the rights
466
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
467
+ * copies of the Software, and to permit persons to whom the Software is
468
+ * furnished to do so, subject to the following conditions:
469
+ *
470
+ * The above copyright notice and this permission notice shall be included in all
471
+ * copies or substantial portions of the Software.
472
+ *
473
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
474
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
475
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
476
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
477
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
478
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
479
+ * SOFTWARE.
480
+ */
481
+
482
+ // empty comment so the license doesn't become the doc comment for this func
483
+
484
+ /***/
485
+ export function getTokenAtPosition(
486
+ parent: Node,
487
+ pos: number,
488
+ sourceFile?: SourceFile,
489
+ allowJsDoc?: boolean,
490
+ ): Node | undefined {
491
+ if (pos < parent.pos || pos >= parent.end) {
492
+ return;
493
+ }
494
+ if (isTokenKind(parent.kind)) {
495
+ return parent;
496
+ }
497
+ return _getTokenAtPosition(parent, pos, sourceFile ?? parent.getSourceFile(), allowJsDoc === true);
498
+ }
499
+
500
+ function _getTokenAtPosition(node: Node, pos: number, sourceFile: SourceFile, allowJsDoc: boolean): Node | undefined {
501
+ if (!allowJsDoc) {
502
+ // if we are not interested in JSDoc, we can skip to the deepest AST node at the given position
503
+ node = getAstNodeAtPosition(node, pos)!;
504
+ if (isTokenKind(node.kind)) {
505
+ return node;
506
+ }
507
+ }
508
+ outer: while (true) {
509
+ for (const child of node.getChildren()) {
510
+ if (child.end > pos && (allowJsDoc || child.kind !== SyntaxKind.JSDoc)) {
511
+ if (isTokenKind(child.kind)) {
512
+ return child;
513
+ }
514
+ node = child;
515
+ continue outer;
516
+ }
517
+ }
518
+ return;
519
+ }
520
+ }
521
+
522
+ /** Returns the deepest AST Node at `pos`. Returns undefined if `pos` is outside of the range of `node` */
523
+ export function getAstNodeAtPosition(node: Node, pos: number): Node | undefined {
524
+ if (node.pos > pos || node.end <= pos) {
525
+ return;
526
+ }
527
+ while (isNodeKind(node.kind)) {
528
+ const nested = forEachChild(node, (child) => (child.pos <= pos && child.end > pos ? child : undefined));
529
+
530
+ if (nested === undefined) {
531
+ break;
532
+ }
533
+ node = nested;
534
+ }
535
+ return node;
536
+ }
537
+
538
+ /**
539
+ * stolen form tsutils, seems sketchy
540
+ */
541
+ function isNodeKind(kind: SyntaxKind) {
542
+ return kind >= SyntaxKind.FirstNode;
543
+ }
544
+
545
+ export function nonNull<T>(x: T | null | undefined): x is T {
546
+ return x != null;
547
+ }
548
+
549
+ export function isEmptyObjectLiteral(node: Node): node is ObjectLiteralExpression {
550
+ return isObjectLiteralExpression(node) && node.properties.length === 0;
551
+ }
552
+
553
+ export function isCommaExpression(node: Node): node is
554
+ & BinaryExpression
555
+ & {
556
+ readonly operatorToken: Token<SyntaxKind.CommaToken>;
557
+ } {
558
+ return isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.CommaToken;
559
+ }
560
+
561
+ export function isInExpression(node: Node): node is
562
+ & BinaryExpression
563
+ & {
564
+ readonly operatorToken: Token<SyntaxKind.InKeyword>;
565
+ } {
566
+ return isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.InKeyword;
567
+ }
568
+
569
+ export function isStatic(item: PropertyDeclaration): boolean {
570
+ return item.modifiers?.some((mod) => isStaticKeyword(mod)) ?? false;
571
+ }