Skip to content

Commit 6e75efc

Browse files
committed
feat(regex101): new command
1 parent fa309b0 commit 6e75efc

18 files changed

+250
-12
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
"peerDependencies": {
8080
"eslint": "*"
8181
},
82+
"dependencies": {
83+
"@es-joy/jsdoccomment": "^0.43.0"
84+
},
8285
"devDependencies": {
8386
"@antfu/eslint-config": "^2.17.0",
8487
"@antfu/ni": "^0.21.12",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { toPromiseAll } from './to-promise-all'
1212
import { noShorthand } from './no-shorthand'
1313
import { noType } from './no-type'
1414
import { keepUnique } from './keep-unique'
15+
import { regex101 } from './regex101'
1516

1617
// @keep-sorted
1718
export {
@@ -20,6 +21,7 @@ export {
2021
keepUnique,
2122
noShorthand,
2223
noType,
24+
regex101,
2325
toArrow,
2426
toDestructuring,
2527
toDynamicImport,
@@ -38,6 +40,7 @@ export const builtinCommands = [
3840
keepUnique,
3941
noShorthand,
4042
noType,
43+
regex101,
4144
toArrow,
4245
toDestructuring,
4346
toDynamicImport,

src/commands/inline-arrow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'
22

33
export const inlineArrow: Command = {
44
name: 'inline-arrow',
5-
match: /^[\/:@]\s*(inline-arrow|ia)$/,
5+
match: /^\s*[\/:@]\s*(inline-arrow|ia)$/,
66
action(ctx) {
77
const arrowFn = ctx.findNodeBelow('ArrowFunctionExpression')
88
if (!arrowFn)

src/commands/no-shorthand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Command } from '../types'
33

44
export const noShorthand: Command = {
55
name: 'no-shorthand',
6-
match: /^[\/:@]\s*(no-shorthand|nsh)$/,
6+
match: /^\s*[\/:@]\s*(no-shorthand|nsh)$/,
77
action(ctx) {
88
const nodes = ctx.findNodeBelow<AST_NODE_TYPES.Property>({
99
filter: node => node.type === 'Property' && node.shorthand,

src/commands/no-type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command } from '../types'
22

33
export const noType: Command = {
44
name: 'no-type',
5-
match: /^[\/:@]\s*(no-type|nt)$/,
5+
match: /^\s*[\/:@]\s*(no-type|nt)$/,
66
action(ctx) {
77
const nodes = ctx.findNodeBelow({
88
filter: node => node.type.startsWith('TS'),

src/commands/regex101.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# `regex101`
2+
3+
Generate up-to-date [regex101](https://regex101.com/) links for your RegExp patterns in jsdoc comments. Helps you test and inspect the RegExp easily.
4+
5+
## Triggers
6+
7+
- `// @regex101`
8+
- `/* @regex101 */`
9+
10+
## Examples
11+
12+
```js
13+
/**
14+
* RegExp to match foo or bar, optionally wrapped in quotes.
15+
*
16+
* @regex101
17+
*/
18+
const foo = /(['"])?(foo|bar)\\1?/gi
19+
```
20+
21+
Will be updated to:
22+
23+
```js
24+
/**
25+
* RegExp to match foo or bar, optionally wrapped in quotes.
26+
*
27+
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript
28+
*/
29+
const foo = /(['"])?(foo|bar)\\1?/gi
30+
```
31+
32+
An whenever you update the RegExp pattern, the link will be updated as well.
33+
34+
### Optional Test Strings
35+
36+
Test string can also be provided via an optional `@example` tag:
37+
38+
```js
39+
/**
40+
* Some jsdoc
41+
*
42+
* @example str
43+
* \`\`\`js
44+
* if ('foo'.match(foo)) {
45+
* const foo = bar
46+
* }
47+
* \`\`\`
48+
*
49+
* @regex101
50+
*/
51+
const foo = /(['"])?(foo|bar)\\1?/gi
52+
```
53+
54+
Will be updated to:
55+
56+
```js
57+
/**
58+
* Some jsdoc
59+
*
60+
* @example str
61+
* \`\`\`js
62+
* if ('foo'.match(foo)) {
63+
* const foo = bar
64+
* }
65+
* \`\`\`
66+
*
67+
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript&testString=if+%28%27foo%27.match%28foo%29%29+%7B%0A++const+foo+%3D+bar%0A%7D
68+
*/
69+
const foo = /(['"])?(foo|bar)\\1?/gi
70+
```

src/commands/regex101.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { regex101 as command } from './regex101'
2+
import { $, run } from './_test-utils'
3+
4+
run(
5+
command,
6+
// basic
7+
{
8+
code: $`
9+
// @regex101
10+
const foo = /(?:\\b|\\s)@regex101(\\s[^\\s]+)?(?:\\s|\\b|$)/g
11+
`,
12+
output: output => expect(output).toMatchInlineSnapshot(`
13+
"// @regex101 https://regex101.com/?regex=%28%3F%3A%5Cb%7C%5Cs%29%40regex101%28%5Cs%5B%5E%5Cs%5D%2B%29%3F%28%3F%3A%5Cs%7C%5Cb%7C%24%29&flags=g&flavor=javascript
14+
const foo = /(?:\\b|\\s)@regex101(\\s[^\\s]+)?(?:\\s|\\b|$)/g"
15+
`),
16+
errors: ['command-fix'],
17+
},
18+
// block comment
19+
{
20+
code: $`
21+
/**
22+
* Some jsdoc
23+
*
24+
* @regex101
25+
* @deprecated
26+
*/
27+
const foo = /(['"])?(foo|bar)\\1?/gi
28+
`,
29+
output: output => expect(output).toMatchInlineSnapshot(`
30+
"/**
31+
* Some jsdoc
32+
*
33+
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript
34+
* @deprecated
35+
*/
36+
const foo = /(['"])?(foo|bar)\\1?/gi"
37+
`),
38+
errors: ['command-fix'],
39+
},
40+
// example block
41+
{
42+
code: $`
43+
/**
44+
* Some jsdoc
45+
*
46+
* @example str
47+
* \`\`\`js
48+
* if ('foo'.match(foo)) {
49+
* const foo = bar
50+
* }
51+
* \`\`\`
52+
*
53+
* @regex101
54+
*/
55+
const foo = /(['"])?(foo|bar)\\1?/gi
56+
`,
57+
output: output => expect(output).toMatchInlineSnapshot(`
58+
"/**
59+
* Some jsdoc
60+
*
61+
* @example str
62+
* \`\`\`js
63+
* if ('foo'.match(foo)) {
64+
* const foo = bar
65+
* }
66+
* \`\`\`
67+
*
68+
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript&testString=if+%28%27foo%27.match%28foo%29%29+%7B%0A++const+foo+%3D+bar%0A%7D
69+
*/
70+
const foo = /(['"])?(foo|bar)\\1?/gi"
71+
`),
72+
errors: ['command-fix'],
73+
},
74+
)

src/commands/regex101.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { parseComment } from '@es-joy/jsdoccomment'
2+
import type { Command, Tree } from '../types'
3+
4+
// @regex101 https://regex101.com/?regex=%60%60%60%28.*%29%5Cn%28%5B%5Cs%5CS%5D*%29%5Cn%60%60%60&flavor=javascript
5+
const reCodeBlock = /```(.*)\n([\s\S]*)\n```/
6+
7+
export const regex101: Command = {
8+
name: 'regex101',
9+
/**
10+
* @regex101 https://regex101.com/?regex=%28%5Cb%7C%5Cs%7C%5E%29%28%40regex101%29%28%5Cs%5B%5E%5Cs%5D%2B%29%3F%28%5Cb%7C%5Cs%7C%24%29&flavor=javascript
11+
*/
12+
match: /(\b|\s|^)(@regex101)(\s[^\s]+)?(\b|\s|$)/,
13+
commentType: 'both',
14+
action(ctx) {
15+
const literal = ctx.findNodeBelow((n) => {
16+
return n.type === 'Literal' && 'regex' in n
17+
}) as Tree.RegExpLiteral | undefined
18+
if (!literal)
19+
return ctx.reportError('Unable to find arrow function to convert')
20+
21+
const [
22+
_fullStr = '',
23+
spaceBefore = '',
24+
commandStr = '',
25+
existingUrl = '',
26+
_spaceAfter = '',
27+
] = ctx.matches as string[]
28+
29+
let example: string | undefined
30+
31+
if (ctx.comment.value.includes('```') && ctx.comment.value.includes('@example')) {
32+
try {
33+
const parsed = parseComment(ctx.comment, '')
34+
const tag = parsed.tags.find(t => t.tag === 'example')
35+
const description = tag?.description
36+
const code = description?.match(reCodeBlock)?.[2].trim()
37+
if (code)
38+
example = code
39+
}
40+
catch (e) {}
41+
}
42+
43+
// docs: https://github.com/firasdib/Regex101/wiki/FAQ#how-to-prefill-the-fields-on-the-interface-via-url
44+
const query = new URLSearchParams()
45+
query.set('regex', literal.regex.pattern)
46+
if (literal.regex.flags)
47+
query.set('flags', literal.regex.flags)
48+
query.set('flavor', 'javascript')
49+
if (example)
50+
query.set('testString', example)
51+
const url = `https://regex101.com/?${query}`
52+
53+
if (existingUrl.trim() === url.trim())
54+
return
55+
56+
const indexStart = ctx.comment.range[0] + ctx.matches.index! + spaceBefore.length + 2 /** comment prefix */
57+
const indexEnd = indexStart + commandStr.length + existingUrl.length
58+
59+
ctx.report({
60+
loc: {
61+
start: ctx.source.getLocFromIndex(indexStart),
62+
end: ctx.source.getLocFromIndex(indexEnd),
63+
},
64+
removeComment: false,
65+
message: `Update the regex101 link`,
66+
fix(fixer) {
67+
return fixer.replaceTextRange([indexStart, indexEnd], `@regex101 ${url}`)
68+
},
69+
})
70+
},
71+
}

src/commands/to-arrow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command } from '../types'
22

33
export const toArrow: Command = {
44
name: 'to-arrow',
5-
match: /^[\/:@]\s*(to-arrow|2a|ta)$/,
5+
match: /^\s*[\/:@]\s*(to-arrow|2a|ta)$/,
66
action(ctx) {
77
const fn = ctx.findNodeBelow('FunctionDeclaration', 'FunctionExpression')
88
if (!fn)

src/commands/to-destructuring.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command } from '../types'
22

33
export const toDestructuring: Command = {
44
name: 'to-destructuring',
5-
match: /^[\/:@]\s*(?:to-|2)?(?:destructuring|dest)?$/i,
5+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:destructuring|dest)?$/i,
66
action(ctx) {
77
const node = ctx.findNodeBelow(
88
'VariableDeclaration',

src/commands/to-dynamic-import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'
22

33
export const toDynamicImport: Command = {
44
name: 'to-dynamic-import',
5-
match: /^[\/:@]\s*(?:to-|2)?(?:dynamic|d)(?:-?import)?$/i,
5+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:dynamic|d)(?:-?import)?$/i,
66
action(ctx) {
77
const node = ctx.findNodeBelow('ImportDeclaration')
88
if (!node)

src/commands/to-for-each.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const FOR_TRAVERSE_IGNORE: NodeType[] = [
1414

1515
export const toForEach: Command = {
1616
name: 'to-for-each',
17-
match: /^[\/:@]\s*(?:to-|2)?(?:for-?each)$/i,
17+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:for-?each)$/i,
1818
action(ctx) {
1919
const node = ctx.findNodeBelow('ForInStatement', 'ForOfStatement')
2020
if (!node)

src/commands/to-for-of.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FOR_TRAVERSE_IGNORE } from './to-for-each'
33

44
export const toForOf: Command = {
55
name: 'to-for-of',
6-
match: /^[\/:@]\s*(?:to-|2)?(?:for-?of)$/i,
6+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:for-?of)$/i,
77
action(ctx) {
88
const target = ctx.findNodeBelow((node) => {
99
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'forEach')

src/commands/to-function.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'
22

33
export const toFunction: Command = {
44
name: 'to-function',
5-
match: /^[\/:@]\s*(to-(?:fn|function)|2f|tf)$/,
5+
match: /^\s*[\/:@]\s*(to-(?:fn|function)|2f|tf)$/,
66
action(ctx) {
77
const arrowFn = ctx.findNodeBelow('ArrowFunctionExpression')
88
if (!arrowFn)

src/commands/to-string-literal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getNodesByIndexes, parseToNumberArray } from './_utils'
33

44
export const toStringLiteral: Command = {
55
name: 'to-string-literal',
6-
match: /^[\/:@]\s*(?:to-|2)?(?:string-literal|sl)\s{0,}(.*)?$/,
6+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:string-literal|sl)\s{0,}(.*)?$/,
77
action(ctx) {
88
const numbers = ctx.matches[1]
99
// From integers 1-based to 0-based to match array indexes

src/commands/to-template-literal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type NodeTypes = Tree.StringLiteral | Tree.BinaryExpression
55

66
export const toTemplateLiteral: Command = {
77
name: 'to-template-literal',
8-
match: /^[\/:@]\s*(?:to-|2)?(?:template-literal|tl)\s{0,}(.*)?$/,
8+
match: /^\s*[\/:@]\s*(?:to-|2)?(?:template-literal|tl)\s{0,}(.*)?$/,
99
action(ctx) {
1010
const numbers = ctx.matches[1]
1111
// From integers 1-based to 0-based to match array indexes

src/rule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function createRuleWithCommands(commands: Command[]) {
2525
const comments = sc.getAllComments()
2626

2727
for (const comment of comments) {
28-
const commandRaw = comment.value.trim()
28+
const commandRaw = comment.value
2929
for (const command of commands) {
3030
const type = command.commentType ?? 'line'
3131
if (type === 'line' && comment.type !== 'Line')

0 commit comments

Comments
 (0)