From 977c435d80f51dee4d03ed6550d898f09d446881 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 21 Feb 2024 16:50:51 -0500 Subject: [PATCH 1/5] feat: add getAccessKind --- src/nodes/access.test.ts | 49 +++++++++ src/nodes/access.ts | 212 +++++++++++++++++++++++++++++++++++++++ src/nodes/index.ts | 1 + 3 files changed, 262 insertions(+) create mode 100644 src/nodes/access.test.ts create mode 100644 src/nodes/access.ts diff --git a/src/nodes/access.test.ts b/src/nodes/access.test.ts new file mode 100644 index 00000000..11c4f530 --- /dev/null +++ b/src/nodes/access.test.ts @@ -0,0 +1,49 @@ +import ts from "typescript"; +import { describe, expect, it } from "vitest"; + +import { createNodeAndSourceFile } from "../test/utils"; +import { AccessKind, getAccessKind } from "./access"; + +describe("getAccessKind", () => { + it("returns AccessKind.None when the node is not an access", () => { + const { node } = createNodeAndSourceFile("let value;"); + + const actual = getAccessKind(node); + + expect(actual).toBe(AccessKind.None); + }); + + it("returns AccessKind.Read when the node is a read", () => { + const { node } = createNodeAndSourceFile("abc.def;"); + + const actual = getAccessKind(node); + + expect(actual).toBe(AccessKind.Read); + }); + + it("returns AccessKind.Write when the node is a write", () => { + const { node } = createNodeAndSourceFile("abc = ghi;"); + + const actual = getAccessKind(node.left); + + expect(actual).toBe(AccessKind.Write); + }); + + it("returns AccessKind.Delete when the node is a delete", () => { + const { node } = + createNodeAndSourceFile("delete abc.def;"); + + const actual = getAccessKind(node.expression); + + expect(actual).toBe(AccessKind.Delete); + }); + + it("returns AccessKind.ReadWrite when the node reads and writes", () => { + const { node } = + createNodeAndSourceFile("abc.def += 1;"); + + const actual = getAccessKind(node.left); + + expect(actual).toBe(AccessKind.ReadWrite); + }); +}); diff --git a/src/nodes/access.ts b/src/nodes/access.ts new file mode 100644 index 00000000..ad386ca8 --- /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, + Write = 2, + Delete = 4, + 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: + // (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 c1122eb9..703aa7e4 100644 --- a/src/nodes/index.ts +++ b/src/nodes/index.ts @@ -1 +1,2 @@ +export * from "./access"; export * from "./typeGuards"; From 4f496157ef675cdca40397bf03c34f995653712c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 22 Feb 2024 09:53:47 -0500 Subject: [PATCH 2/5] Added some more testing --- src/nodes/access.test.ts | 80 ++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/src/nodes/access.test.ts b/src/nodes/access.test.ts index 11c4f530..b9c9c370 100644 --- a/src/nodes/access.test.ts +++ b/src/nodes/access.test.ts @@ -1,28 +1,72 @@ import ts from "typescript"; import { describe, expect, it } from "vitest"; -import { createNodeAndSourceFile } from "../test/utils"; +import { createNode, createNode } from "../test/utils"; import { AccessKind, getAccessKind } from "./access"; +function createNodeAndToString(sourceText: string) { + const node = createNode(sourceText); + return { + node, + toString: () => node.getText(), + }; +} + describe("getAccessKind", () => { it("returns AccessKind.None when the node is not an access", () => { - const { node } = createNodeAndSourceFile("let value;"); + const node = createNode("let value;"); const actual = getAccessKind(node); expect(actual).toBe(AccessKind.None); }); - it("returns AccessKind.Read when the node is a read", () => { - const { node } = createNodeAndSourceFile("abc.def;"); + it("returns AccessKind.Read when the node is a property access", () => { + const node = createNode("abc.def;"); const actual = getAccessKind(node); expect(actual).toBe(AccessKind.Read); }); - it("returns AccessKind.Write when the node is a write", () => { - const { node } = createNodeAndSourceFile("abc = ghi;"); + it("returns AccessKind.Read when the node is a shorthand property assignment in a variable", () => { + const node = createNode("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("[...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.Write when the node is a binary assignment", () => { + const node = createNode("abc = ghi;"); const actual = getAccessKind(node.left); @@ -30,20 +74,34 @@ describe("getAccessKind", () => { }); it("returns AccessKind.Delete when the node is a delete", () => { - const { node } = - createNodeAndSourceFile("delete abc.def;"); + const node = createNode("delete abc.def;"); const actual = getAccessKind(node.expression); expect(actual).toBe(AccessKind.Delete); }); - it("returns AccessKind.ReadWrite when the node reads and writes", () => { - const { node } = - createNodeAndSourceFile("abc.def += 1;"); + it("returns AccessKind.ReadWrite when the node is a binary read-write operator", () => { + const node = createNode("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("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("++abc++"); + + const actual = getAccessKind(node.operand); + + expect(actual).toBe(AccessKind.ReadWrite); + }); }); From 89cf051424643f6dfed3b1a46f746877cf7808d6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 22 Feb 2024 10:01:03 -0500 Subject: [PATCH 3/5] Added some more testing, again --- src/nodes/access.test.ts | 42 ++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/nodes/access.test.ts b/src/nodes/access.test.ts index b9c9c370..35ffde94 100644 --- a/src/nodes/access.test.ts +++ b/src/nodes/access.test.ts @@ -4,14 +4,6 @@ import { describe, expect, it } from "vitest"; import { createNode, createNode } from "../test/utils"; import { AccessKind, getAccessKind } from "./access"; -function createNodeAndToString(sourceText: string) { - const node = createNode(sourceText); - return { - node, - toString: () => node.getText(), - }; -} - describe("getAccessKind", () => { it("returns AccessKind.None when the node is not an access", () => { const node = createNode("let value;"); @@ -65,6 +57,40 @@ describe("getAccessKind", () => { 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("abc = ghi;"); From 32618db3c37e45290126ade29763187bbda14d9d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 22 Feb 2024 10:02:07 -0500 Subject: [PATCH 4/5] Fixed up --- src/nodes/access.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nodes/access.test.ts b/src/nodes/access.test.ts index 35ffde94..67c8929a 100644 --- a/src/nodes/access.test.ts +++ b/src/nodes/access.test.ts @@ -1,7 +1,7 @@ import ts from "typescript"; import { describe, expect, it } from "vitest"; -import { createNode, createNode } from "../test/utils"; +import { createNode } from "../test/utils"; import { AccessKind, getAccessKind } from "./access"; describe("getAccessKind", () => { From 4f0ccafced44540517b456b50a1f997e309b1b2e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 9 Mar 2024 08:19:32 -0500 Subject: [PATCH 5/5] Use bitwise notation, nice --- src/nodes/access.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nodes/access.ts b/src/nodes/access.ts index ad386ca8..38aff061 100644 --- a/src/nodes/access.ts +++ b/src/nodes/access.ts @@ -11,9 +11,9 @@ import { isAssignmentKind } from "../syntax"; */ export enum AccessKind { None = 0, - Read = 1, - Write = 2, - Delete = 4, + Read = 1 << 0, + Write = 1 << 1, + Delete = 1 << 2, ReadWrite = Read | Write, /* eslint-enable perfectionist/sort-enums */ }