Skip to content

Commit 2f8c76a

Browse files
feat: add getAccessKind (#398)
## PR Checklist - [x] Addresses an existing open issue: fixes #397 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/ts-api-utils/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/ts-api-utils/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Adds the function, with some rudimentary unit tests.
1 parent 3b8419b commit 2f8c76a

3 files changed

Lines changed: 346 additions & 0 deletions

File tree

src/nodes/access.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import ts from "typescript";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { createNode } from "../test/utils";
5+
import { AccessKind, getAccessKind } from "./access";
6+
7+
describe("getAccessKind", () => {
8+
it("returns AccessKind.None when the node is not an access", () => {
9+
const node = createNode<ts.Expression>("let value;");
10+
11+
const actual = getAccessKind(node);
12+
13+
expect(actual).toBe(AccessKind.None);
14+
});
15+
16+
it("returns AccessKind.Read when the node is a property access", () => {
17+
const node = createNode<ts.Expression>("abc.def;");
18+
19+
const actual = getAccessKind(node);
20+
21+
expect(actual).toBe(AccessKind.Read);
22+
});
23+
24+
it("returns AccessKind.Read when the node is a shorthand property assignment in a variable", () => {
25+
const node = createNode<ts.VariableStatement>("const abc = { def };");
26+
27+
const actual = getAccessKind(
28+
node.declarationList.declarations[0].initializer!,
29+
);
30+
31+
expect(actual).toBe(AccessKind.Read);
32+
});
33+
34+
it("returns AccessKind.Read when the node is an array spread element", () => {
35+
const node = createNode<ts.ArrayLiteralExpression>("[...abc]");
36+
37+
const actual = getAccessKind(node.elements[0]);
38+
39+
expect(actual).toBe(AccessKind.Read);
40+
});
41+
42+
it("returns AccessKind.Read when the node is a nested array spread element", () => {
43+
const node = createNode<
44+
ts.ArrayLiteralExpression & {
45+
elements: [
46+
ts.SpreadElement & {
47+
expression: ts.ArrayLiteralExpression & {
48+
elements: ts.SpreadElement;
49+
};
50+
},
51+
];
52+
}
53+
>("[...[...abc]]");
54+
55+
const actual = getAccessKind(node.elements[0].expression.elements[0]);
56+
57+
expect(actual).toBe(AccessKind.Read);
58+
});
59+
60+
it("returns AccessKind.Read when the node is an array spread inside a for-of", () => {
61+
const node = createNode<
62+
ts.ForOfStatement & { expression: ts.ArrayLiteralExpression }
63+
>("for (const _ of [...abc]) {}");
64+
65+
const actual = getAccessKind(node.expression.elements[0]);
66+
67+
expect(actual).toBe(AccessKind.Read);
68+
});
69+
70+
it("returns AccessKind.Read when the node is an array spread inside a binary expression", () => {
71+
const node = createNode<
72+
ts.BinaryExpression & { right: ts.ArrayLiteralExpression }
73+
>("abc = [...def]");
74+
75+
const actual = getAccessKind(node.right.elements[0]);
76+
77+
expect(actual).toBe(AccessKind.Read);
78+
});
79+
80+
it("returns AccessKind.Read when the node is an array spread inside an array expression", () => {
81+
const node = createNode<
82+
ts.BinaryExpression & {
83+
right: ts.ArrayLiteralExpression & {
84+
elements: [ts.ArrayLiteralExpression];
85+
};
86+
}
87+
>("abc = [[...def]]");
88+
89+
const actual = getAccessKind(node.right.elements[0].elements[0]);
90+
91+
expect(actual).toBe(AccessKind.Read);
92+
});
93+
94+
it("returns AccessKind.Write when the node is a binary assignment", () => {
95+
const node = createNode<ts.BinaryExpression>("abc = ghi;");
96+
97+
const actual = getAccessKind(node.left);
98+
99+
expect(actual).toBe(AccessKind.Write);
100+
});
101+
102+
it("returns AccessKind.Delete when the node is a delete", () => {
103+
const node = createNode<ts.DeleteExpression>("delete abc.def;");
104+
105+
const actual = getAccessKind(node.expression);
106+
107+
expect(actual).toBe(AccessKind.Delete);
108+
});
109+
110+
it("returns AccessKind.ReadWrite when the node is a binary read-write operator", () => {
111+
const node = createNode<ts.BinaryExpression>("abc.def += 1;");
112+
113+
const actual = getAccessKind(node.left);
114+
115+
expect(actual).toBe(AccessKind.ReadWrite);
116+
});
117+
118+
it("returns AccessKind.ReadWrite when the node is a postfix unary expression", () => {
119+
const node = createNode<ts.PostfixUnaryExpression>("abc++;");
120+
121+
const actual = getAccessKind(node.operand);
122+
123+
expect(actual).toBe(AccessKind.ReadWrite);
124+
});
125+
126+
it("returns AccessKind.ReadWrite when the node is a prefix unary expression", () => {
127+
const node = createNode<ts.PostfixUnaryExpression>("++abc++");
128+
129+
const actual = getAccessKind(node.operand);
130+
131+
expect(actual).toBe(AccessKind.ReadWrite);
132+
});
133+
});

src/nodes/access.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Code largely based on https://github.com/ajafff/tsutils
2+
// Original license: https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE
3+
4+
import ts from "typescript";
5+
6+
import { isAssignmentKind } from "../syntax";
7+
8+
/* eslint-disable perfectionist/sort-enums */
9+
/**
10+
* What operations(s), if any, an expression applies.
11+
*/
12+
export enum AccessKind {
13+
None = 0,
14+
Read = 1 << 0,
15+
Write = 1 << 1,
16+
Delete = 1 << 2,
17+
ReadWrite = Read | Write,
18+
/* eslint-enable perfectionist/sort-enums */
19+
}
20+
21+
/**
22+
* Determines which operation(s), if any, an expression applies.
23+
* @example
24+
* ```ts
25+
* declare const node: ts.Expression;
26+
*
27+
* if (getAccessKind(node).Write & AccessKind.Write) !== 0) {
28+
* // this is a reassignment (write)
29+
* }
30+
* ```
31+
*/
32+
export function getAccessKind(node: ts.Expression): AccessKind {
33+
const parent = node.parent;
34+
switch (parent.kind) {
35+
case ts.SyntaxKind.DeleteExpression:
36+
return AccessKind.Delete;
37+
case ts.SyntaxKind.PostfixUnaryExpression:
38+
return AccessKind.ReadWrite;
39+
case ts.SyntaxKind.PrefixUnaryExpression:
40+
return (parent as ts.PrefixUnaryExpression).operator ===
41+
ts.SyntaxKind.PlusPlusToken ||
42+
(parent as ts.PrefixUnaryExpression).operator ===
43+
ts.SyntaxKind.MinusMinusToken
44+
? AccessKind.ReadWrite
45+
: AccessKind.Read;
46+
case ts.SyntaxKind.BinaryExpression:
47+
return (parent as ts.BinaryExpression).right === node
48+
? AccessKind.Read
49+
: !isAssignmentKind((parent as ts.BinaryExpression).operatorToken.kind)
50+
? AccessKind.Read
51+
: (parent as ts.BinaryExpression).operatorToken.kind ===
52+
ts.SyntaxKind.EqualsToken
53+
? AccessKind.Write
54+
: AccessKind.ReadWrite;
55+
case ts.SyntaxKind.ShorthandPropertyAssignment:
56+
return (parent as ts.ShorthandPropertyAssignment)
57+
.objectAssignmentInitializer === node
58+
? AccessKind.Read
59+
: isInDestructuringAssignment(parent as ts.ShorthandPropertyAssignment)
60+
? AccessKind.Write
61+
: AccessKind.Read;
62+
case ts.SyntaxKind.PropertyAssignment:
63+
return (parent as ts.PropertyAssignment).name === node
64+
? AccessKind.None
65+
: isInDestructuringAssignment(parent as ts.PropertyAssignment)
66+
? AccessKind.Write
67+
: AccessKind.Read;
68+
case ts.SyntaxKind.ArrayLiteralExpression:
69+
case ts.SyntaxKind.SpreadElement:
70+
case ts.SyntaxKind.SpreadAssignment:
71+
return isInDestructuringAssignment(
72+
parent as
73+
| ts.ArrayLiteralExpression
74+
| ts.SpreadAssignment
75+
| ts.SpreadElement,
76+
)
77+
? AccessKind.Write
78+
: AccessKind.Read;
79+
case ts.SyntaxKind.ParenthesizedExpression:
80+
case ts.SyntaxKind.NonNullExpression:
81+
case ts.SyntaxKind.TypeAssertionExpression:
82+
case ts.SyntaxKind.AsExpression:
83+
// (<number>foo! as {})++
84+
return getAccessKind(parent as ts.Expression);
85+
case ts.SyntaxKind.ForOfStatement:
86+
case ts.SyntaxKind.ForInStatement:
87+
return (parent as ts.ForInOrOfStatement).initializer === node
88+
? AccessKind.Write
89+
: AccessKind.Read;
90+
case ts.SyntaxKind.ExpressionWithTypeArguments:
91+
return (
92+
(parent as ts.ExpressionWithTypeArguments).parent as ts.HeritageClause
93+
).token === ts.SyntaxKind.ExtendsKeyword &&
94+
parent.parent.parent.kind !== ts.SyntaxKind.InterfaceDeclaration
95+
? AccessKind.Read
96+
: AccessKind.None;
97+
case ts.SyntaxKind.ComputedPropertyName:
98+
case ts.SyntaxKind.ExpressionStatement:
99+
case ts.SyntaxKind.TypeOfExpression:
100+
case ts.SyntaxKind.ElementAccessExpression:
101+
case ts.SyntaxKind.ForStatement:
102+
case ts.SyntaxKind.IfStatement:
103+
case ts.SyntaxKind.DoStatement:
104+
case ts.SyntaxKind.WhileStatement:
105+
case ts.SyntaxKind.SwitchStatement:
106+
case ts.SyntaxKind.WithStatement:
107+
case ts.SyntaxKind.ThrowStatement:
108+
case ts.SyntaxKind.CallExpression:
109+
case ts.SyntaxKind.NewExpression:
110+
case ts.SyntaxKind.TaggedTemplateExpression:
111+
case ts.SyntaxKind.JsxExpression:
112+
case ts.SyntaxKind.Decorator:
113+
case ts.SyntaxKind.TemplateSpan:
114+
case ts.SyntaxKind.JsxOpeningElement:
115+
case ts.SyntaxKind.JsxSelfClosingElement:
116+
case ts.SyntaxKind.JsxSpreadAttribute:
117+
case ts.SyntaxKind.VoidExpression:
118+
case ts.SyntaxKind.ReturnStatement:
119+
case ts.SyntaxKind.AwaitExpression:
120+
case ts.SyntaxKind.YieldExpression:
121+
case ts.SyntaxKind.ConditionalExpression:
122+
case ts.SyntaxKind.CaseClause:
123+
case ts.SyntaxKind.JsxElement:
124+
return AccessKind.Read;
125+
case ts.SyntaxKind.ArrowFunction:
126+
return (parent as ts.ArrowFunction).body === node
127+
? AccessKind.Read
128+
: AccessKind.Write;
129+
case ts.SyntaxKind.PropertyDeclaration:
130+
case ts.SyntaxKind.VariableDeclaration:
131+
case ts.SyntaxKind.Parameter:
132+
case ts.SyntaxKind.EnumMember:
133+
case ts.SyntaxKind.BindingElement:
134+
case ts.SyntaxKind.JsxAttribute:
135+
return (parent as ts.JsxAttribute).initializer === node
136+
? AccessKind.Read
137+
: AccessKind.None;
138+
case ts.SyntaxKind.PropertyAccessExpression:
139+
return (parent as ts.PropertyAccessExpression).expression === node
140+
? AccessKind.Read
141+
: AccessKind.None;
142+
case ts.SyntaxKind.ExportAssignment:
143+
return (parent as ts.ExportAssignment).isExportEquals
144+
? AccessKind.Read
145+
: AccessKind.None;
146+
}
147+
148+
return AccessKind.None;
149+
}
150+
151+
function isInDestructuringAssignment(
152+
node:
153+
| ts.ArrayLiteralExpression
154+
| ts.ObjectLiteralExpression
155+
| ts.PropertyAssignment
156+
| ts.ShorthandPropertyAssignment
157+
| ts.SpreadAssignment
158+
| ts.SpreadElement,
159+
): boolean {
160+
switch (node.kind) {
161+
case ts.SyntaxKind.ShorthandPropertyAssignment:
162+
if (node.objectAssignmentInitializer !== undefined) {
163+
return true;
164+
}
165+
166+
// falls through
167+
case ts.SyntaxKind.PropertyAssignment:
168+
case ts.SyntaxKind.SpreadAssignment:
169+
node = node.parent as
170+
| ts.ArrayLiteralExpression
171+
| ts.ObjectLiteralExpression;
172+
break;
173+
case ts.SyntaxKind.SpreadElement:
174+
if (node.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
175+
return false;
176+
}
177+
178+
node = node.parent;
179+
}
180+
181+
while (true) {
182+
switch (node.parent.kind) {
183+
case ts.SyntaxKind.BinaryExpression:
184+
return (
185+
(node.parent as ts.BinaryExpression).left === node &&
186+
(node.parent as ts.BinaryExpression).operatorToken.kind ===
187+
ts.SyntaxKind.EqualsToken
188+
);
189+
case ts.SyntaxKind.ForOfStatement:
190+
return (node.parent as ts.ForOfStatement).initializer === node;
191+
case ts.SyntaxKind.ArrayLiteralExpression:
192+
case ts.SyntaxKind.ObjectLiteralExpression:
193+
node = node.parent as
194+
| ts.ArrayLiteralExpression
195+
| ts.ObjectLiteralExpression;
196+
break;
197+
case ts.SyntaxKind.SpreadAssignment:
198+
case ts.SyntaxKind.PropertyAssignment:
199+
node = node.parent.parent as ts.ObjectLiteralExpression;
200+
break;
201+
case ts.SyntaxKind.SpreadElement:
202+
if (node.parent.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
203+
return false;
204+
}
205+
206+
node = node.parent.parent as ts.ArrayLiteralExpression;
207+
break;
208+
default:
209+
return false;
210+
}
211+
}
212+
}

src/nodes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from "./access";
12
export * from "./typeGuards";

0 commit comments

Comments
 (0)