Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getAccessKind #398

Merged
merged 6 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be better to express these in bitwise notation, to make it clear that's how they are supposed to be used?

export enum AccessKind {
  None = 0,
	Read = 1 << 0,
	Write = 1 << 1,
	Delete = 1 << 2,
	ReadWrite = Read | Write,
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a bit unnecessary though.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

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

Check warning on line 43 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L42-L43

Added lines #L42 - L43 were not covered by tests
? AccessKind.ReadWrite
: AccessKind.Read;

Check warning on line 45 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L45

Added line #L45 was not covered by tests
case ts.SyntaxKind.BinaryExpression:
return (parent as ts.BinaryExpression).right === node
? AccessKind.Read

Check warning on line 48 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L48

Added line #L48 was not covered by tests
: !isAssignmentKind((parent as ts.BinaryExpression).operatorToken.kind)
? AccessKind.Read

Check warning on line 50 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L50

Added line #L50 was not covered by tests
: (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;

Check warning on line 61 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L56-L61

Added lines #L56 - L61 were not covered by tests
case ts.SyntaxKind.PropertyAssignment:
return (parent as ts.PropertyAssignment).name === node
? AccessKind.None
: isInDestructuringAssignment(parent as ts.PropertyAssignment)
? AccessKind.Write
: AccessKind.Read;

Check warning on line 67 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L63-L67

Added lines #L63 - L67 were not covered by tests
case ts.SyntaxKind.ArrayLiteralExpression:
case ts.SyntaxKind.SpreadElement:
case ts.SyntaxKind.SpreadAssignment:
return isInDestructuringAssignment(
parent as
| ts.ArrayLiteralExpression
| ts.SpreadAssignment
| ts.SpreadElement,
)
? AccessKind.Write

Check warning on line 77 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L77

Added line #L77 was not covered by tests
: 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);

Check warning on line 84 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L83-L84

Added lines #L83 - L84 were not covered by tests
case ts.SyntaxKind.ForOfStatement:
case ts.SyntaxKind.ForInStatement:
return (parent as ts.ForInOrOfStatement).initializer === node
? AccessKind.Write
: AccessKind.Read;

Check warning on line 89 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L87-L89

Added lines #L87 - L89 were not covered by tests
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;

Check warning on line 96 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L91-L96

Added lines #L91 - L96 were not covered by tests
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;

Check warning on line 128 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L126-L128

Added lines #L126 - L128 were not covered by tests
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;

Check warning on line 137 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L137

Added line #L137 was not covered by tests
case ts.SyntaxKind.PropertyAccessExpression:
return (parent as ts.PropertyAccessExpression).expression === node
? AccessKind.Read
: AccessKind.None;

Check warning on line 141 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L139-L141

Added lines #L139 - L141 were not covered by tests
case ts.SyntaxKind.ExportAssignment:
return (parent as ts.ExportAssignment).isExportEquals
? AccessKind.Read
: AccessKind.None;

Check warning on line 145 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L143-L145

Added lines #L143 - L145 were not covered by tests
}

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;
}

Check warning on line 164 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L162-L164

Added lines #L162 - L164 were not covered by tests

// falls through
case ts.SyntaxKind.PropertyAssignment:
case ts.SyntaxKind.SpreadAssignment:
node = node.parent as
| ts.ArrayLiteralExpression
| ts.ObjectLiteralExpression;
break;

Check warning on line 172 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L169-L172

Added lines #L169 - L172 were not covered by tests
case ts.SyntaxKind.SpreadElement:
if (node.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
return false;
}

node = node.parent;

Check warning on line 178 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L174-L178

Added lines #L174 - L178 were not covered by tests
}

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

Check warning on line 187 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L186-L187

Added lines #L186 - L187 were not covered by tests
);
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;

Check warning on line 200 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L199-L200

Added lines #L199 - L200 were not covered by tests
case ts.SyntaxKind.SpreadElement:
if (node.parent.parent.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
return false;
}

Check warning on line 204 in src/nodes/access.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/access.ts#L203-L204

Added lines #L203 - L204 were not covered by tests

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";

Check warning on line 1 in src/nodes/index.ts

View check run for this annotation

Codecov / codecov/patch

src/nodes/index.ts#L1

Added line #L1 was not covered by tests
export * from "./typeGuards";
Loading