Skip to content

Commit

Permalink
feat: add getAccessKind (#398)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
JoshuaKGoldberg authored Mar 9, 2024
1 parent 3b8419b commit 2f8c76a
Show file tree
Hide file tree
Showing 3 changed files with 346 additions and 0 deletions.
133 changes: 133 additions & 0 deletions src/nodes/access.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
212 changes: 212 additions & 0 deletions src/nodes/access.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions src/nodes/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./access";
export * from "./typeGuards";

0 comments on commit 2f8c76a

Please sign in to comment.