From 7e1324b40d0f36134cd1bf830660b322513e2a29 Mon Sep 17 00:00:00 2001 From: Alexandre Stahmer Date: Tue, 7 Nov 2023 04:03:38 +0100 Subject: [PATCH] feat: refine + add TODOs and README --- .changeset/itchy-sheep-crash.md | 26 +++++++ README.md | 122 ++++++++++++++++++++++++++++++++ src/pattern-matching.ts | 24 +++++++ tests/pattern-matching.test.ts | 26 ++++++- 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-sheep-crash.md create mode 100644 README.md diff --git a/.changeset/itchy-sheep-crash.md b/.changeset/itchy-sheep-crash.md new file mode 100644 index 0000000..0aa90c3 --- /dev/null +++ b/.changeset/itchy-sheep-crash.md @@ -0,0 +1,26 @@ +--- +'typemorph': patch +--- + +Add `ast.refine` + +```ts +const code = ` + another(1, true, 3, "str") + someFn() + find({ id: 1 }) + ` + +const sourceFile = parse(code) +const pattern = traverse( + sourceFile, + ast.refine(ast.callExpression('find'), (node) => node.getArguments()[0]), +) + +// Pattern { +// "matchKind": "ObjectLiteralExpression", +// "text": "{ id: 1 }", +// "line": 4, +// "column": 53 +// } +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f0a49e --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# TypeMorph Library + +TypeMorph is a TypeScript library designed to streamline working with abstract syntax trees (ASTs). With a focus on +pattern matching, TypeMorph simplifies the process of analyzing and manipulating TypeScript code. + +## Pattern Matching + +TypeMorph provides the `ast` class for creating pattern matchers for TypeScript syntax, enabling complex AST queries and +transformations to be expressed in a simple and concise manner. + +## Installation + +```sh +pnpm install typemorph +``` + +## Usage + +Here is a basic example of how to use TypeMorph to match and manipulate TypeScript AST nodes: + +```typescript +import { ast } from 'typemorph' + +// Match a string literal +const stringMatcher = ast.string('hello') + +// Match a numeric literal +const numberMatcher = ast.number(42) + +// Match any node +const anyMatcher = ast.any() + +// Will resolve to any node with a name or identifier of 'something' +const namedMatcher = ast.named('something') + +// Match a type-only import declaration +ast.importDeclaration('node:path', 'path', true) + +// Match a named import declaration +ast.importDeclaration('node:fs', ['writeFile', 'readFile']) + +// Match using a tuple pattern +ast.importDeclaration('node:fs', ast.tuple(ast.importSpecifier('writeFile'), ast.importSpecifier('readFile'))) + +// Match an import declaration with a rest pattern +ast.importDeclaration('node:fs', ast.rest(ast.any())) +``` + +## Examples + +### Example 1: Escape hatch + +Anytime you need to match a node that does not have a dedicated method, you can use the `ast.node` matcher to match any +AST node of a specific kind. + +```ts +const specificImportSpecifierMatcher = ast.node(SyntaxKind.ImportSpecifier, { + name: ast.identifier('specificName'), // Match only import specifiers with the name "specificName". + propertyName: ast.identifier('specificPropertyName'), // Match only if the property name is "specificPropertyName". +}) +``` + +### Example 1: Flexible Patterns + +```ts +const flexibleMatcherWithRest = ast.importDeclaration( + 'node:fs', + ast.rest(ast.any()), // This will match any number of import specifiers in the import. +) +``` + +### Example 2: Refining Matchers + +```ts +const typeImportMatcher = ast.refine( + ast.importDeclaration(ast.any(), ast.any(), true), // This matches any import declaration that is a type import. + (importDeclarationNode) => { + // This function can further process the node if needed. + return importDeclarationNode.isTypeOnly() // Returns true if the import is type-only. + }, +) +``` + +### Example 3: Combining Matchers for Complex Patterns + +```ts +const functionReturningPromiseOfSpecificTypeMatcher = ast.node(SyntaxKind.FunctionDeclaration, { + name: ast.identifier('myFunction'), // Match a function named "myFunction". + type: ast.node(SyntaxKind.TypeReference, { + // Match the return type. + typeName: ast.identifier('Promise'), + typeArguments: ast.tuple( + ast.node(SyntaxKind.TypeReference, { + typeName: ast.identifier('MySpecificType'), // Match "Promise" that resolves to "MySpecificType". + }), + ), + }), +}) +``` + +For more advanced use-cases, refer to the ~~detailed API documentation provided with the library~~ test folder. + +## Contributing + +Contributions are welcome! If you have an idea for an improvement or have found a bug, please open an issue or submit a +pull request. + +- `pnpm i` +- `pnpm build` +- `pnpm test` +- `pnpm changeset` + +When you're done with your changes, please run `pnpm changeset` in the root of the repo and follow the instructions +described [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md). + +tl;dr: `pnpm changeset` will create a new changeset file in the `.changeset` folder. Please commit this file along with +your changes. Don't consume the changeset, as this will be done by the CI. + +--- + +Please refer to the actual codebase of TypeMorph for more complex and detailed patterns and utilities. This README +provides a starting point for understanding and integrating the library into your projects. diff --git a/src/pattern-matching.ts b/src/pattern-matching.ts index 791ecb5..c134a17 100644 --- a/src/pattern-matching.ts +++ b/src/pattern-matching.ts @@ -45,6 +45,8 @@ export type NodeParams = { [K in ExtractNodeKeys>]?: Pattern } +export type PatternNode = TPattern extends Pattern ? TMatch : never + export class ast { static kind(syntaxKind: SyntaxKind) { return new Pattern({ kind: syntaxKind, match: Node.isNode }) @@ -92,6 +94,20 @@ export class ast { }) } + static refine( + pattern: TPattern, + transform: (node: PatternNode) => RNode | undefined, + ) { + return new Pattern, RNode>({ + kind: pattern.kind as any, + match: (node) => { + if (Array.isArray(node)) return + if (!pattern.matchFn(node)) return + return transform(node as PatternNode) + }, + }) + } + static named(name: string) { return new Pattern({ kind: SyntaxKind.Unknown, @@ -456,6 +472,14 @@ export class ast { }) } + // TODO block + variablestatement + variable declaration + expressionstatement + return + // TODO if statement + else statement + else if statement + // TODO arrow function / function declaration + parameter + // TODO class delcaration + method declaration + new expression + // TODO type parameter + type reference + literaltype + indexedaccesstype + // TODO as expression + type assertion + non null assertion + parenthesized expression + satisfies expression + prefix unary expression + // TODO property assignment + shorthand property assignment + spread assignment + // TODO resolve identifier declaration, resolve static value, resolve TS type // find unresolvable() } diff --git a/tests/pattern-matching.test.ts b/tests/pattern-matching.test.ts index 5e2cfa1..7a29998 100644 --- a/tests/pattern-matching.test.ts +++ b/tests/pattern-matching.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' import { createProject } from './create-project' -import { SourceFile, ts, Node, Identifier } from 'ts-morph' +import { SourceFile, ts, Node, Identifier, ObjectLiteralExpression } from 'ts-morph' import { Pattern, ast } from '../src/pattern-matching' const project = createProject() @@ -100,6 +100,30 @@ test('ast.when', () => { expect(pattern?.match?.getText()).toMatchInlineSnapshot('"find"') }) +test('ast.refine', () => { + const code = ` + another(1, true, 3, "str") + someFn() + find({ id: 1 }) + ` + + const sourceFile = parse(code) + const pattern = traverse( + sourceFile, + ast.refine(ast.callExpression('find'), (node) => node.getArguments()[0]), + ) + + expect(pattern).toMatchInlineSnapshot(` + Pattern { + "matchKind": "ObjectLiteralExpression", + "text": "{ id: 1 }", + "line": 4, + "column": 53 + } + `) + expect(pattern?.match?.getText()).toMatchInlineSnapshot('"{ id: 1 }"') +}) + test('ast.named', () => { const code = ` import xxx from "some-module"