Repository: JoshuaKGoldberg/ts-api-utils (ts)

Issue:
🚀 Feature: Add getAccessKind
### Bug Report Checklist

- [X] I have tried restarting my IDE and the issue persists.
- [X] I have pulled the latest `main` branch of the repository.
- [X] I have [searched for related issues](https://github.com/JoshuaKGoldberg/ts-api-utils/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

I took a peek through the [typescript-eslint `yarn.lock`](https://github.com/typescript-eslint/typescript-eslint/blob/b615fc295c05599085979e2064c6907645715e21/yarn.lock) to see who depends on `tsutils` still. [`eslint-plugin-deprecation`](https://github.com/gund/eslint-plugin-deprecation) does still for exactly one function: [`import { isReassignmentTarget } from 'tsutils';`](https://github.com/gund/eslint-plugin-deprecation/blob/5a32ce0c761782902c07ec9df4dee5150f7b5c4c/src/rules/deprecation.ts#L22).

[`isReassignmentTarget` is a one-liner in `tsutils`](https://github.com/ajafff/tsutils/blob/03b4df15d14702f9c7a128ac3fae77171778d613/util/util.ts#L1172-L1174) that calls to a [much larger `getAccessKind function`](https://github.com/ajafff/tsutils/blob/03b4df15d14702f9c7a128ac3fae77171778d613/util/util.ts#L1064).

### Additional Info

This is essentially a split-off from #3, long after I'd thought #3 was resolved. 😄 

Pull request description:
feat: add getAccessKind
## 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.

Patch:
diff --git a/README.md b/README.md
index c853c4c..04753e2 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 <p align="center">
 	Utility functions for working with TypeScript's API.
-	Successor to the wonderful Successor to the wonderful <a href="https://github.com/ajafff/tsutils">tsutils</a>. 🛠️️
+	Successor to the wonderful <a href="https://github.com/ajafff/tsutils">tsutils</a>. 🛠️️
 </p>
 
 <p align="center">
diff --git a/src/nodes/access.ts b/src/nodes/access.ts
new file mode 100644
index 0000000..38aff06
--- /dev/null
+++ b/src/nodes/access.ts
@@ -0,0 +1,212 @@
+// Code largely based on https://github.com/ajafff/tsutils
+// Original license: https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE
+
+import ts from "typescript";
+
+import { isAssignmentKind } from "../syntax";
+
+/* eslint-disable perfectionist/sort-enums */
+/**
+ * What operations(s), if any, an expression applies.
+ */
+export enum AccessKind {
+	None = 0,
+	Read = 1 << 0,
+	Write = 1 << 1,
+	Delete = 1 << 2,
+	ReadWrite = Read | Write,
+	/* eslint-enable perfectionist/sort-enums */
+}
+
+/**
+ * Determines which operation(s), if any, an expression applies.
+ * @example
+ * ```ts
+ * declare const node: ts.Expression;
+ *
+ * if (getAccessKind(node).Write & AccessKind.Write) !== 0) {
+ *   // this is a reassignment (write)
+ * }
+ * ```
+ */
+export function getAccessKind(node: ts.Expression): AccessKind {
+	const parent = node.parent;
+	switch (parent.kind) {
+		case ts.SyntaxKind.DeleteExpression:
+			return AccessKind.Delete;
+		case ts.SyntaxKind.PostfixUnaryExpression:
+			return AccessKind.ReadWrite;
+		case ts.SyntaxKind.PrefixUnaryExpression:
+			return (parent as ts.PrefixUnaryExpression).operator ===
+				ts.SyntaxKind.PlusPlusToken ||
+				(parent as ts.PrefixUnaryExpression).operator ===
+					ts.SyntaxKind.MinusMinusToken
+				? AccessKind.ReadWrite
+				: AccessKind.Read;
+		case ts.SyntaxKind.BinaryExpression:
+			return (parent as ts.BinaryExpression).right === node
+				? AccessKind.Read
+				: !isAssignmentKind((parent as ts.BinaryExpression).operatorToken.kind)
+					? AccessKind.Read
+					: (parent as ts.BinaryExpression).operatorToken.kind ===
+						  ts.SyntaxKind.EqualsToken
+						? AccessKind.Write
+						: AccessKind.ReadWrite;
+		case ts.SyntaxKind.ShorthandPropertyAssignment:
+			return (parent as ts.ShorthandPropertyAssignment)
+				.objectAssignmentInitializer === node
+				? AccessKind.Read
+				: isInDestructuringAssignment(parent as ts.ShorthandPropertyAssignment)
+					? AccessKind.Write
+					: AccessKind.Read;
+		case ts.SyntaxKind.PropertyAssignment:
+			return (parent as ts.PropertyAssignment).name === node
+				? AccessKind.None
+				: isInDestructuringAssignment(parent as ts.PropertyAssignment)
+					? AccessKind.Write
+					: AccessKind.Read;
+		case ts.SyntaxKind.ArrayLiteralExpression:
+		case ts.SyntaxKind.SpreadElement:
+		case ts.SyntaxKind.SpreadAssignment:
+			return isInDestructuringAssignment(
+				parent as
+					| ts.ArrayLiteralExpression
+					| ts.SpreadAssignment
+					| ts.SpreadElement,
+			)
+				? AccessKind.Write
+				: AccessKind.Read;
+		case ts.SyntaxKind.ParenthesizedExpression:
+		case ts.SyntaxKind.NonNullExpression:
+		case ts.SyntaxKind.TypeAssertionExpression:
+		case ts.SyntaxKind.AsExpression:
+			// (<number>foo! as {})++
+			return getAccessKind(parent as ts.Expression);
+		case ts.SyntaxKind.ForOfStatement:
+		case ts.SyntaxKind.ForInStatement:
+			return (parent as ts.ForInOrOfStatement).initializer === node
+				? AccessKind.Write
+				: AccessKind.Read;
+		case ts.SyntaxKind.ExpressionWithTypeArguments:
+			return (
+				(parent as ts.ExpressionWithTypeArguments).parent as ts.HeritageClause
+			).token === ts.SyntaxKind.ExtendsKeyword &&
+				parent.parent.parent.kind !== ts.SyntaxKind.InterfaceDeclaration
+				? AccessKind.Read
+				: AccessKind.None;
+		case ts.SyntaxKind.ComputedPropertyName:
+		case ts.SyntaxKind.ExpressionStatement:
+		case ts.SyntaxKind.TypeOfExpression:
+		case ts.SyntaxKind.ElementAccessExpression:
+		case ts.SyntaxKind.ForStatement:
+		case ts.SyntaxKind.IfStatement:
+		case ts.SyntaxKind.DoStatement:
+		case ts.SyntaxKind.WhileStatement:
+		case ts.SyntaxKind.SwitchStatement:
+		case ts.SyntaxKind.WithStatement:
+		case ts.SyntaxKind.ThrowStatement:
+		case ts.SyntaxKind.CallExpression:
+		case ts.SyntaxKind.NewExpression:
+		case ts.SyntaxKind.TaggedTemplateExpression:
+		case ts.SyntaxKind.JsxExpression:
+		case ts.SyntaxKind.Decorator:
+		case ts.SyntaxKind.TemplateSpan:
+		case ts.SyntaxKind.JsxOpeningElement:
+		case ts.SyntaxKind.JsxSelfClosingElement:
+		case ts.SyntaxKind.JsxSpreadAttribute:
+		case ts.SyntaxKind.VoidExpression:
+		case ts.SyntaxKind.ReturnStatement:
+		case ts.SyntaxKind.AwaitExpression:
+		case ts.SyntaxKind.YieldExpression:
+		case ts.SyntaxKind.ConditionalExpression:
+		case ts.SyntaxKind.CaseClause:
+		case ts.SyntaxKind.JsxElement:
+			return AccessKind.Read;
+		case ts.SyntaxKind.ArrowFunction:
+			return (parent as ts.ArrowFunction).body === node
+				? AccessKind.Read
+				: AccessKind.Write;
+		case ts.SyntaxKind.PropertyDeclaration:
+		case ts.SyntaxKind.VariableDeclaration:
+		case ts.SyntaxKind.Parameter:
+		case ts.SyntaxKind.EnumMember:
+		case ts.SyntaxKind.BindingElement:
+		case ts.SyntaxKind.JsxAttribute:
+			return (parent as ts.JsxAttribute).initializer === node
+				? AccessKind.Read
+				: AccessKind.None;
+		case ts.SyntaxKind.PropertyAccessExpression:
+			return (parent as ts.PropertyAccessExpression).expression === node
+				? AccessKind.Read
+				: AccessKind.None;
+		case ts.SyntaxKind.ExportAssignment:
+			return (parent as ts.ExportAssignment).isExportEquals
+				? AccessKind.Read
+				: AccessKind.None;
+	}
+
+	return AccessKind.None;
+}
+
+function isInDestructuringAssignment(
+	node:
+		| ts.ArrayLiteralExpression
+		| ts.ObjectLiteralExpression
+		| ts.PropertyAssignment
+		| ts.ShorthandPropertyAssignment
+		| ts.SpreadAssignment
+		| ts.SpreadElement,
+): boolean {
+	switch (node.kind) {
+		case ts.SyntaxKind.ShorthandPropertyAssignment:
+			if (node.objectAssignmentInitializer !== undefined) {
+				return true;
+			}
+
+		// falls through
+		case ts.SyntaxKind.PropertyAssignment:
+		case ts.SyntaxKind.SpreadAssignment:
+			node = node.parent as
+				| ts.ArrayLiteralExpression
+				| ts.ObjectLiteralExpression;
+			break;
+		case ts.SyntaxKind.SpreadElement:
+			if (node.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
+				return false;
+			}
+
+			node = node.parent;
+	}
+
+	while (true) {
+		switch (node.parent.kind) {
+			case ts.SyntaxKind.BinaryExpression:
+				return (
+					(node.parent as ts.BinaryExpression).left === node &&
+					(node.parent as ts.BinaryExpression).operatorToken.kind ===
+						ts.SyntaxKind.EqualsToken
+				);
+			case ts.SyntaxKind.ForOfStatement:
+				return (node.parent as ts.ForOfStatement).initializer === node;
+			case ts.SyntaxKind.ArrayLiteralExpression:
+			case ts.SyntaxKind.ObjectLiteralExpression:
+				node = node.parent as
+					| ts.ArrayLiteralExpression
+					| ts.ObjectLiteralExpression;
+				break;
+			case ts.SyntaxKind.SpreadAssignment:
+			case ts.SyntaxKind.PropertyAssignment:
+				node = node.parent.parent as ts.ObjectLiteralExpression;
+				break;
+			case ts.SyntaxKind.SpreadElement:
+				if (node.parent.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
+					return false;
+				}
+
+				node = node.parent.parent as ts.ArrayLiteralExpression;
+				break;
+			default:
+				return false;
+		}
+	}
+}
diff --git a/src/nodes/index.ts b/src/nodes/index.ts
index c1122eb..703aa7e 100644
--- a/src/nodes/index.ts
+++ b/src/nodes/index.ts
@@ -1,1 +1,2 @@
+export * from "./access";
 export * from "./typeGuards";


Test patch:
diff --git a/src/nodes/access.test.ts b/src/nodes/access.test.ts
new file mode 100644
index 0000000..67c8929
--- /dev/null
+++ b/src/nodes/access.test.ts
@@ -0,0 +1,133 @@
+import ts from "typescript";
+import { describe, expect, it } from "vitest";
+
+import { createNode } from "../test/utils";
+import { AccessKind, getAccessKind } from "./access";
+
+describe("getAccessKind", () => {
+	it("returns AccessKind.None when the node is not an access", () => {
+		const node = createNode<ts.Expression>("let value;");
+
+		const actual = getAccessKind(node);
+
+		expect(actual).toBe(AccessKind.None);
+	});
+
+	it("returns AccessKind.Read when the node is a property access", () => {
+		const node = createNode<ts.Expression>("abc.def;");
+
+		const actual = getAccessKind(node);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is a shorthand property assignment in a variable", () => {
+		const node = createNode<ts.VariableStatement>("const abc = { def };");
+
+		const actual = getAccessKind(
+			node.declarationList.declarations[0].initializer!,
+		);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is an array spread element", () => {
+		const node = createNode<ts.ArrayLiteralExpression>("[...abc]");
+
+		const actual = getAccessKind(node.elements[0]);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is a nested array spread element", () => {
+		const node = createNode<
+			ts.ArrayLiteralExpression & {
+				elements: [
+					ts.SpreadElement & {
+						expression: ts.ArrayLiteralExpression & {
+							elements: ts.SpreadElement;
+						};
+					},
+				];
+			}
+		>("[...[...abc]]");
+
+		const actual = getAccessKind(node.elements[0].expression.elements[0]);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is an array spread inside a for-of", () => {
+		const node = createNode<
+			ts.ForOfStatement & { expression: ts.ArrayLiteralExpression }
+		>("for (const _ of [...abc]) {}");
+
+		const actual = getAccessKind(node.expression.elements[0]);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is an array spread inside a binary expression", () => {
+		const node = createNode<
+			ts.BinaryExpression & { right: ts.ArrayLiteralExpression }
+		>("abc = [...def]");
+
+		const actual = getAccessKind(node.right.elements[0]);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Read when the node is an array spread inside an array expression", () => {
+		const node = createNode<
+			ts.BinaryExpression & {
+				right: ts.ArrayLiteralExpression & {
+					elements: [ts.ArrayLiteralExpression];
+				};
+			}
+		>("abc = [[...def]]");
+
+		const actual = getAccessKind(node.right.elements[0].elements[0]);
+
+		expect(actual).toBe(AccessKind.Read);
+	});
+
+	it("returns AccessKind.Write when the node is a binary assignment", () => {
+		const node = createNode<ts.BinaryExpression>("abc = ghi;");
+
+		const actual = getAccessKind(node.left);
+
+		expect(actual).toBe(AccessKind.Write);
+	});
+
+	it("returns AccessKind.Delete when the node is a delete", () => {
+		const node = createNode<ts.DeleteExpression>("delete abc.def;");
+
+		const actual = getAccessKind(node.expression);
+
+		expect(actual).toBe(AccessKind.Delete);
+	});
+
+	it("returns AccessKind.ReadWrite when the node is a binary read-write operator", () => {
+		const node = createNode<ts.BinaryExpression>("abc.def += 1;");
+
+		const actual = getAccessKind(node.left);
+
+		expect(actual).toBe(AccessKind.ReadWrite);
+	});
+
+	it("returns AccessKind.ReadWrite when the node is a postfix unary expression", () => {
+		const node = createNode<ts.PostfixUnaryExpression>("abc++;");
+
+		const actual = getAccessKind(node.operand);
+
+		expect(actual).toBe(AccessKind.ReadWrite);
+	});
+
+	it("returns AccessKind.ReadWrite when the node is a prefix unary expression", () => {
+		const node = createNode<ts.PostfixUnaryExpression>("++abc++");
+
+		const actual = getAccessKind(node.operand);
+
+		expect(actual).toBe(AccessKind.ReadWrite);
+	});
+});