Skip to content

Commit 217b398

Browse files
committed
feat(hoist-regexp): new command, close #16
1 parent 91053cd commit 217b398

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

src/commands/hoist-regexp.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# `hoist-regexp`
2+
3+
Hoist regular expressions to the top-level.
4+
5+
## Triggers
6+
7+
- `/// hoist-regexp`
8+
- `/// hoist-regex`
9+
- `/// hreg`
10+
11+
## Examples
12+
13+
```js
14+
function foo(msg: string): void {
15+
/// hoist-regexp
16+
console.log(/foo/.test(msg))
17+
}
18+
```
19+
20+
Will be converted to:
21+
22+
```js
23+
const re$0 = /foo/
24+
25+
function foo(msg: string): void {
26+
console.log(re$0.test(msg))
27+
}
28+
```
29+
30+
You can also provide a name for the hoisted regular expression:
31+
32+
```js
33+
function foo(msg: string): void {
34+
/// hoist-regexp myRegex
35+
console.log(/foo/.test(msg))
36+
}
37+
```
38+
39+
Will be converted to:
40+
41+
```js
42+
const myRegex = /foo/
43+
44+
function foo(msg: string): void {
45+
console.log(myRegex.test(msg))
46+
}
47+
```

src/commands/hoist-regexp.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { hoistRegExp as command } from './hoist-regexp'
2+
import { $, run } from './_test-utils'
3+
4+
run(
5+
command,
6+
// basic
7+
{
8+
code: $`
9+
function foo(msg: string): void {
10+
/// hoist-regexp
11+
console.log(/foo/.test(msg))
12+
}
13+
`,
14+
output: $`
15+
const reFoo = /foo/
16+
function foo(msg: string): void {
17+
console.log(reFoo.test(msg))
18+
}
19+
`,
20+
errors: ['command-fix'],
21+
},
22+
// custom name
23+
{
24+
code: $`
25+
function foo(msg: string): void {
26+
/// hoist-regex customName
27+
console.log(/foo/.test(msg))
28+
}
29+
`,
30+
output: $`
31+
const customName = /foo/
32+
function foo(msg: string): void {
33+
console.log(customName.test(msg))
34+
}
35+
`,
36+
errors: ['command-fix'],
37+
},
38+
// nested functions
39+
{
40+
code: $`
41+
const bar = 1
42+
function bar(msg: string): void {
43+
}
44+
45+
function foo(msg: string): void {
46+
const bar = () => {
47+
for (let i = 0; i < 10; i++) {
48+
/// hreg
49+
console.log(/foo|bar*([^a])/.test(msg))
50+
}
51+
}
52+
}
53+
`,
54+
output: $`
55+
const bar = 1
56+
function bar(msg: string): void {
57+
}
58+
59+
const reFoo_bar_a = /foo|bar*([^a])/
60+
function foo(msg: string): void {
61+
const bar = () => {
62+
for (let i = 0; i < 10; i++) {
63+
console.log(reFoo_bar_a.test(msg))
64+
}
65+
}
66+
}
67+
`,
68+
errors: ['command-fix'],
69+
},
70+
// throw error if variable already exists
71+
{
72+
code: $`
73+
function foo(msg: string): void {
74+
const customName = 42
75+
/// hoist-regex customName
76+
console.log(/foo/.test(msg))
77+
}
78+
`,
79+
errors: ['command-error'],
80+
},
81+
// throw error if it's already top-level
82+
{
83+
code: $`
84+
/// hoist-regex
85+
const customName = /foo/
86+
function foo(msg: string): void {
87+
console.log(/foo/.test(msg))
88+
}
89+
`,
90+
errors: ['command-error'],
91+
},
92+
)

src/commands/hoist-regexp.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Command, Tree } from '../types'
2+
3+
export const hoistRegExp: Command = {
4+
name: 'hoist-regexp',
5+
match: /^\s*[/:@]\s*(?:hoist-|h)reg(?:exp?)?(?:\s+(\S+)\s*)?$/,
6+
action(ctx) {
7+
const regexNode = ctx.findNodeBelow((node): node is Tree.RegExpLiteral => node.type === 'Literal' && 'regex' in node) as Tree.RegExpLiteral
8+
if (!regexNode)
9+
return ctx.reportError('No regular expression literal found')
10+
11+
const topNodes = ctx.source.ast.body as Tree.Node[]
12+
const scope = ctx.source.getScope(regexNode)
13+
14+
let parent = regexNode.parent as Tree.Node | undefined
15+
while (parent && !topNodes.includes(parent))
16+
parent = parent.parent
17+
if (!parent)
18+
return ctx.reportError('Failed to find top-level node')
19+
20+
let name = ctx.matches[1]
21+
if (name) {
22+
if (scope.references.find(ref => ref.identifier.name === name))
23+
return ctx.reportError(`Variable '${name}' is already in scope`)
24+
}
25+
else {
26+
let baseName = regexNode.regex.pattern
27+
.replace(/\W/g, '_')
28+
.replace(/_{2,}/g, '_')
29+
.replace(/^_+|_+$/, '')
30+
.toLowerCase()
31+
32+
if (baseName.length > 0)
33+
baseName = baseName[0].toUpperCase() + baseName.slice(1)
34+
35+
let i = 0
36+
name = `re${baseName}`
37+
while (scope.references.find(ref => ref.identifier.name === name)) {
38+
i++
39+
name = `${baseName}${i}`
40+
}
41+
}
42+
43+
ctx.report({
44+
node: regexNode,
45+
message: `Hoist regular expression to ${name}`,
46+
*fix(fixer) {
47+
yield fixer.insertTextBefore(parent, `const ${name} = ${ctx.source.getText(regexNode)}\n`)
48+
yield fixer.replaceText(regexNode, name)
49+
},
50+
})
51+
},
52+
}

0 commit comments

Comments
 (0)