Skip to content

Commit

Permalink
test: ast.is/with/named
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer committed Nov 29, 2023
1 parent bc5fefe commit ff936e4
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 7 deletions.
34 changes: 33 additions & 1 deletion src/pattern-matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ interface ListOptions {
}

export class ast {
/**
* Only matches the given node kind
*/
static kind<TKind extends SyntaxKind>(syntaxKind: TKind) {
const matcher = Node.is(syntaxKind)
return new Pattern({
Expand All @@ -180,18 +183,25 @@ export class ast {
})
}

/**
* Takes an existing pattern and further refines it with the given AST properties
*/
static is<TPattern extends Pattern>(pattern: TPattern, props?: NodeParams<PatternKind<TPattern>>) {
const matcher = ast.with(props)

return new Pattern({
params: { pattern, props },
kind: SyntaxKind.Unknown,
match: (node) => {
return matcher.match(node)
return pattern.match(node) && matcher.match(node)
},
})
}

/**
* Matches any node with the given AST properties
* The point is to use this in combination with other patterns that already asserted a set of node kind
*/
static with<TKind extends SyntaxKind>(props?: NodeParams<TKind>) {
const _props = (props ? compact(props) : undefined) as NodeParams<TKind> | undefined
return new Pattern({
Expand Down Expand Up @@ -242,6 +252,9 @@ export class ast {
})
}

/**
* Asserts that the node is of the given kind with the given AST properties
*/
static node<TKind extends SyntaxKind>(type: TKind, props?: NodeParams<TKind>) {
const pattern = ast.with(props)

Expand Down Expand Up @@ -330,10 +343,14 @@ export class ast {
})
}

/** Will match any node or nodeList (as long as it's not undefined) */
static any() {
return new Pattern({ kind: SyntaxKind.Unknown, match: () => true })
}

/**
* Negates the given pattern
*/
static not<TPattern extends Pattern>(pattern: TPattern) {
return new Pattern({
params: { pattern },
Expand All @@ -344,6 +361,10 @@ export class ast {
})
}

/**
* Matches a node if it contains a descendant matching the given pattern,
* optionally stops the traversal when the "until" pattern matches
*/
static contains<TInside extends Pattern, TUntil extends Pattern>(pattern: TInside, until?: TUntil) {
const seen = new WeakSet()

Expand Down Expand Up @@ -372,6 +393,9 @@ export class ast {
})
}

/**
* Matches a node with a custom function
*/
static when<TInput = Node | Node[]>(condition: (node: TInput) => boolean | Node | Node[] | undefined) {
return new Pattern({
params: { condition },
Expand All @@ -397,6 +421,14 @@ export class ast {
})
}

/**
* Matches a node if it has the given name
* @example ast.named('foo') -> matches `foo` in `const foo = 1`
* @example ast.named('foo') -> matches `foo` in `const { foo } = obj`
* @example ast.named('foo') -> matches `foo` in `foo?.()`
* @example ast.named('foo') -> matches `foo` in `import foo from 'bar'`
*
*/
static named(name: string) {
return new Pattern({
kind: SyntaxKind.Unknown,
Expand Down
183 changes: 177 additions & 6 deletions tests/pattern-matching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,78 @@ test('ast.node - no match', () => {
expect(pattern).toMatchInlineSnapshot('undefined')
})

test('ast.is', () => {
const code = `
someFn()
another(1, true, 3, "str")
find({ id: 1 })
`

const sourceFile = parse(code)

expect(
traverse(
sourceFile,
ast.is(ast.kind(ts.SyntaxKind.CallExpression), {
expression: ast.identifier('another'),
}),
),
).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"pattern": "CallExpression",
"props": {
"expression": "Identifier"
}
},
"matches": [
{
"kind": "CallExpression",
"text": "another(1, true, 3, \\"str\\")",
"line": 3,
"column": 18
}
]
}
`)
})

test('ast.with', () => {
const code = `
someFn()
another(1, true, 3, "str")
find({ id: 1 })
`

const sourceFile = parse(code)

const pattern = ast.kind(ts.SyntaxKind.CallExpression)
const propsPattern = ast.with({
expression: ast.identifier('another'),
})

expect(
traverse(
sourceFile,
ast.refine(pattern, (node) => !Array.isArray(node) && propsPattern.match(node)),
),
).toMatchInlineSnapshot(`
Pattern<CallExpression> {
"params": {
"pattern": "CallExpression"
},
"matches": [
{
"kind": "CallExpression",
"text": "another(1, true, 3, \\"str\\")",
"line": 3,
"column": 18
}
]
}
`)
})

test('ast.nodeList', () => {
const code = `
someFn({})
Expand Down Expand Up @@ -1079,18 +1151,19 @@ test('ast.maybeNode', () => {
`)
})

test('ast.named', () => {
const code = `
describe('ast.named', () => {
test('identifier', () => {
const code = `
import xxx from "some-module"
another(1, true, 3, "str")
someFn()
find({ id: 1 })
`

const sourceFile = parse(code)
const sourceFile = parse(code)

expect(traverse(sourceFile, ast.named('find'))).toMatchInlineSnapshot(`
expect(traverse(sourceFile, ast.named('find'))).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"name": "find"
Expand All @@ -1105,9 +1178,19 @@ test('ast.named', () => {
]
}
`)
})

const someModule = traverse(sourceFile, ast.named('xxx'))
expect(someModule).toMatchInlineSnapshot(`
test('import clause', () => {
const code = `
import xxx from "some-module"
another(1, true, 3, "str")
someFn()
find({ id: 1 })
`

const sourceFile = parse(code)
expect(traverse(sourceFile, ast.named('xxx'))).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"name": "xxx"
Expand All @@ -1122,6 +1205,94 @@ test('ast.named', () => {
]
}
`)
})

test('variable declaration', () => {
const code = `
import xxx from "some-module"
const foo = 1
another(1, true, 3, "str")
someFn()
find({ id: 1 })
`

const sourceFile = parse(code)

expect(traverse(sourceFile, ast.named('foo'))).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"name": "foo"
},
"matches": [
{
"kind": "VariableDeclaration",
"text": "foo = 1",
"line": 4,
"column": 36
}
]
}
`)
})

test('variable declaration', () => {
const code = `
import xxx from "some-module"
const foo = 1
another(1, true, 3, "str")
someFn()
find({ id: 1 })
`

const sourceFile = parse(code)

expect(traverse(sourceFile, ast.named('foo'))).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"name": "foo"
},
"matches": [
{
"kind": "VariableDeclaration",
"text": "foo = 1",
"line": 4,
"column": 36
}
]
}
`)
})

test('object binding pattern', () => {
const code = `
import xxx from "some-module"
const { bar } = obj
another(1, true, 3, "str")
someFn()
find({ id: 1 })
`

const sourceFile = parse(code)

expect(traverse(sourceFile, ast.named('bar'))).toMatchInlineSnapshot(`
Pattern<Unknown> {
"params": {
"name": "bar"
},
"matches": [
{
"kind": "BindingElement",
"text": "bar",
"line": 4,
"column": 36
}
]
}
`)
})
})

test('ast.identifier', () => {
Expand Down

0 comments on commit ff936e4

Please sign in to comment.