Skip to content

Commit 8f053b0

Browse files
committed
feat: support predicates in .find, .closest, .destruct
1 parent f33fb60 commit 8f053b0

File tree

6 files changed

+214
-56
lines changed

6 files changed

+214
-56
lines changed

.vscode/snippets.json.code-snippets

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"prefix": "parse-testcase",
44
"body": [
55
"import { parseTestcase } from '../parseTestcase'",
6+
"import dedent from 'dedent-js'",
67
"",
78
"parseTestcase({",
89
" file: __filename,",
9-
" input: `",
10+
" input: dedent`",
1011
" $0",
1112
" `,",
1213
" expected: [",
@@ -20,13 +21,14 @@
2021
"prefix": "find-testcase",
2122
"body": [
2223
"import { findReplaceTestcase } from '../findReplaceTestcase'",
24+
"import dedent from 'dedent-js'",
2325
"",
2426
"findReplaceTestcase({",
2527
" file: __filename,",
26-
" input: `",
28+
" input: dedent`",
2729
" $0",
2830
" `,",
29-
" find: `",
31+
" find: dedent`",
3032
" ",
3133
" `,",
3234
" expectedFind: [",
@@ -40,22 +42,23 @@
4042
"prefix": "find-replace-testcase",
4143
"body": [
4244
"import { findReplaceTestcase } from '../findReplaceTestcase'",
45+
"import dedent from 'dedent-js'",
4346
"",
4447
"findReplaceTestcase({",
4548
" file: __filename,",
46-
" input: `",
49+
" input: dedent`",
4750
" $0",
4851
" `,",
49-
" find: `",
52+
" find: dedent`",
5053
" ",
5154
" `,",
5255
" expectedFind: [",
5356
" ",
5457
" ],",
55-
" replace: `",
58+
" replace: dedent`",
5659
" ",
5760
" `,",
58-
" expectedReplace: `",
61+
" expectedReplace: dedent`",
5962
" ",
6063
" `,",
6164
"})",
@@ -66,19 +69,20 @@
6669
"prefix": "replace-testcase",
6770
"body": [
6871
"import { findReplaceTestcase } from '../findReplaceTestcase'",
72+
"import dedent from 'dedent-js'",
6973
"",
7074
"findReplaceTestcase({",
7175
" file: __filename,",
72-
" input: `",
76+
" input: dedent`",
7377
" $0",
7478
" `,",
75-
" find: `",
79+
" find: dedent`",
7680
" ",
7781
" `,",
78-
" replace: `",
82+
" replace: dedent`",
7983
" ",
8084
" `,",
81-
" expectedReplace: `",
85+
" expectedReplace: dedent`",
8286
" ",
8387
" `,",
8488
"})",
@@ -90,16 +94,17 @@
9094
"body": [
9195
"import { TransformOptions } from '../../src'",
9296
"import { astxTestcase } from '../astxTestcase'",
97+
"import dedent from 'dedent-js'",
9398
"",
9499
"astxTestcase({",
95100
" file: __filename,",
96-
" input: `",
101+
" input: dedent`",
97102
" $0",
98103
" `,",
99104
" astx: ({ astx }: TransformOptions): void => {",
100105
" ",
101106
" },",
102-
" expected: `",
107+
" expected: dedent`",
103108
" ",
104109
" `,",
105110
"})",

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ For example if you do `astx.find('foo($$args)').find('$a + $b')`, the second `fi
537537
You can call `.find` as a method or tagged template literal:
538538

539539
- `` .find`pattern`(options?: FindOptions) ``
540-
- `.find(pattern: string | string[] | Node | Node[] | NodePath | NodePath[], options?: FindOptions)`
540+
- `.find(pattern: string | string[] | Node | Node[] | NodePath | NodePath[] | ((wrapper: Astx) => boolean), options?: FindOptions)`
541541

542542
If you give the pattern as a string, it must be a valid expression or statement(s). Otherwise it should be valid
543543
AST node(s) you already parsed or constructed.

src/Astx.ts

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Expression, Statement, Node, NodePath } from './types'
22
import { Backend } from './backend/Backend'
33
import find, { Match, convertWithCaptures, createMatch } from './find'
44
import replace from './replace'
5-
import compileMatcher, { MatchResult } from './compileMatcher'
5+
import compileMatcher, { CompiledMatcher, MatchResult } from './compileMatcher'
66
import CodeFrameError from './util/CodeFrameError'
77
import ensureArray from './util/ensureArray'
88
import * as AstTypes from 'ast-types'
@@ -13,6 +13,7 @@ import {
1313
getRestPlaceholder,
1414
} from './compileMatcher/Placeholder'
1515
import { SimpleReplacementInterface } from './util/SimpleReplacementCollector'
16+
import forEachNode from './util/forEachNode'
1617

1718
export type TransformOptions = {
1819
/** The absolute path to the current file. */
@@ -60,6 +61,8 @@ export type GetReplacement = (
6061
parse: ParsePattern
6162
) => string | Node | Node[]
6263

64+
export type FindPredicate = (wrapper: Astx) => boolean
65+
6366
function isNode(x: unknown): x is Node {
6467
return x instanceof Object && typeof (x as any).type === 'string'
6568
}
@@ -374,12 +377,75 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
374377
}
375378
}
376379

380+
private _execPatternOrPredicate<Options>(
381+
name: string,
382+
exec: (match: CompiledMatcher['match'], options?: Options) => Astx,
383+
arg0:
384+
| string
385+
| Node
386+
| Node[]
387+
| NodePath<any>
388+
| NodePath<any>[]
389+
| string[]
390+
| TemplateStringsArray
391+
| FindPredicate,
392+
...rest: any[]
393+
): Astx | ((options?: Options) => Astx) {
394+
const { backend } = this
395+
if (arg0 instanceof Function) {
396+
const predicate = arg0
397+
const options = rest[0]
398+
const match = (path: NodePath): MatchResult => {
399+
const wrapper = new Astx(this.context, [path], {
400+
withCaptures: this._matches,
401+
})
402+
return predicate(wrapper) ? wrapper.initialMatch || {} : null
403+
}
404+
try {
405+
return exec(match, options)
406+
} catch (error) {
407+
if (error instanceof Error) {
408+
CodeFrameError.rethrow(error, {
409+
filename: `${name} pattern`,
410+
})
411+
}
412+
throw error
413+
}
414+
} else {
415+
return this._execPattern(
416+
name,
417+
(
418+
pattern: NodePath<Node, any> | readonly NodePath<Node, any>[],
419+
options?: Options
420+
) => {
421+
pattern = ensureArray(pattern)
422+
if (pattern.length !== 1) {
423+
throw new Error(`must be a single node`)
424+
}
425+
const matcher = compileMatcher(pattern[0], {
426+
...options,
427+
backend,
428+
})
429+
return exec(matcher.match, options)
430+
},
431+
arg0,
432+
...rest
433+
)
434+
}
435+
}
436+
377437
closest(
378438
strings: TemplateStringsArray,
379439
...quasis: any[]
380440
): (options?: FindOptions) => Astx
381441
closest(
382-
pattern: string | Node | Node[] | NodePath<any> | NodePath<any>[],
442+
pattern:
443+
| string
444+
| Node
445+
| Node[]
446+
| NodePath<any>
447+
| NodePath<any>[]
448+
| FindPredicate,
383449
options?: FindOptions
384450
): Astx
385451
closest(
@@ -389,39 +455,27 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
389455
| Node[]
390456
| NodePath<any>
391457
| NodePath<any>[]
392-
| TemplateStringsArray,
458+
| TemplateStringsArray
459+
| FindPredicate,
393460
...rest: any[]
394461
): Astx | ((options?: FindOptions) => Astx) {
395-
const { context, backend } = this
396-
return this._execPattern(
462+
const { context } = this
463+
return this._execPatternOrPredicate(
397464
'closest',
398-
(
399-
pattern: NodePath<Node, any> | readonly NodePath<Node, any>[],
400-
options?: FindOptions
401-
): Astx => {
402-
pattern = ensureArray(pattern)
403-
if (pattern.length !== 1) {
404-
throw new Error(`must be a single node`)
405-
}
406-
const matcher = compileMatcher(pattern[0], {
407-
...options,
408-
backend,
409-
})
410-
465+
(matcher: CompiledMatcher['match']): Astx => {
411466
const matchedParents: Set<NodePath> = new Set()
412467
const matches: Match[] = []
413468
this.paths.forEach((path) => {
414469
for (let p = path.parentPath; p; p = p.parentPath) {
415470
if (matchedParents.has(p)) return
416-
const match = matcher.match(p, this.initialMatch)
471+
const match = matcher(p, this.initialMatch)
417472
if (match) {
418473
matchedParents.add(p)
419474
matches.push(createMatch(p, match))
420475
return
421476
}
422477
}
423478
})
424-
425479
return new Astx(context, matches)
426480
},
427481
arg0,
@@ -434,7 +488,13 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
434488
...quasis: any[]
435489
): (options?: FindOptions) => Astx
436490
destruct(
437-
pattern: string | Node | Node[] | NodePath<any> | NodePath<any>[],
491+
pattern:
492+
| string
493+
| Node
494+
| Node[]
495+
| NodePath<any>
496+
| NodePath<any>[]
497+
| FindPredicate,
438498
options?: FindOptions
439499
): Astx
440500
destruct(
@@ -444,31 +504,19 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
444504
| Node[]
445505
| NodePath<any>
446506
| NodePath<any>[]
447-
| TemplateStringsArray,
507+
| TemplateStringsArray
508+
| FindPredicate,
448509
...rest: any[]
449510
): Astx | ((options?: FindOptions) => Astx) {
450-
const { context, backend } = this
451-
return this._execPattern(
511+
const { context } = this
512+
return this._execPatternOrPredicate(
452513
'destruct',
453-
(
454-
pattern: NodePath<Node, any> | readonly NodePath<Node, any>[],
455-
options?: FindOptions
456-
): Astx => {
457-
pattern = ensureArray(pattern)
458-
if (pattern.length !== 1) {
459-
throw new Error(`must be a single node`)
460-
}
461-
const matcher = compileMatcher(pattern[0], {
462-
...options,
463-
backend,
464-
})
465-
514+
(matcher: CompiledMatcher['match']): Astx => {
466515
const matches: Match[] = []
467516
this.paths.forEach((path) => {
468-
const match = matcher.match(path, this.initialMatch)
517+
const match = matcher(path, this.initialMatch)
469518
if (match) matches.push(createMatch(path, match))
470519
})
471-
472520
return new Astx(context, matches)
473521
},
474522
arg0,
@@ -481,7 +529,13 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
481529
...quasis: any[]
482530
): (options?: FindOptions) => Astx
483531
find(
484-
pattern: string | Node | Node[] | NodePath<any> | NodePath<any>[],
532+
pattern:
533+
| string
534+
| Node
535+
| Node[]
536+
| NodePath<any>
537+
| NodePath<any>[]
538+
| FindPredicate,
485539
options?: FindOptions
486540
): Astx
487541
find(
@@ -492,10 +546,24 @@ export default class Astx extends ExtendableProxy implements Iterable<Astx> {
492546
| NodePath<any>
493547
| NodePath<any>[]
494548
| string[]
495-
| TemplateStringsArray,
549+
| TemplateStringsArray
550+
| FindPredicate,
496551
...rest: any[]
497552
): Astx | ((options?: FindOptions) => Astx) {
498553
const { context, backend } = this
554+
if (arg0 instanceof Function) {
555+
const predicate = arg0
556+
const matches: Match[] = []
557+
forEachNode(backend.t, this.paths, ['Node'], (path: NodePath) => {
558+
const wrapper = new Astx(this.context, [path], {
559+
withCaptures: this._matches,
560+
})
561+
if (predicate(wrapper)) {
562+
matches.push(createMatch(path, wrapper.initialMatch || {}))
563+
}
564+
})
565+
return new Astx(context, matches)
566+
}
499567
return this._execPattern(
500568
'find',
501569
(

0 commit comments

Comments
 (0)