Skip to content

Commit

Permalink
feat: refine + add TODOs and README
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer committed Nov 7, 2023
1 parent d4fd06c commit 7e1324b
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 1 deletion.
26 changes: 26 additions & 0 deletions .changeset/itchy-sheep-crash.md
Original file line number Diff line number Diff line change
@@ -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<CallExpression> {
// "matchKind": "ObjectLiteralExpression",
// "text": "{ id: 1 }",
// "line": 4,
// "column": 53
// }
```
122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 24 additions & 0 deletions src/pattern-matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export type NodeParams<TKind extends SyntaxKind> = {
[K in ExtractNodeKeys<CompilerNodeOfKind<TKind>>]?: Pattern<TKind, any>
}

export type PatternNode<TPattern extends Pattern> = TPattern extends Pattern<infer _, infer TMatch> ? TMatch : never

export class ast {
static kind(syntaxKind: SyntaxKind) {
return new Pattern({ kind: syntaxKind, match: Node.isNode })
Expand Down Expand Up @@ -92,6 +94,20 @@ export class ast {
})
}

static refine<TPattern extends Pattern, RNode extends Node>(
pattern: TPattern,
transform: (node: PatternNode<TPattern>) => RNode | undefined,
) {
return new Pattern<ReturnType<RNode['getKind']>, RNode>({
kind: pattern.kind as any,
match: (node) => {
if (Array.isArray(node)) return
if (!pattern.matchFn(node)) return
return transform(node as PatternNode<TPattern>)
},
})
}

static named(name: string) {
return new Pattern({
kind: SyntaxKind.Unknown,
Expand Down Expand Up @@ -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()
}
Expand Down
26 changes: 25 additions & 1 deletion tests/pattern-matching.test.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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<CallExpression> {
"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"
Expand Down

0 comments on commit 7e1324b

Please sign in to comment.