Skip to content

Commit

Permalink
feat(regex101): new command
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed May 8, 2024
1 parent fa309b0 commit 6e75efc
Show file tree
Hide file tree
Showing 18 changed files with 250 additions and 12 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
"peerDependencies": {
"eslint": "*"
},
"dependencies": {
"@es-joy/jsdoccomment": "^0.43.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.17.0",
"@antfu/ni": "^0.21.12",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { toPromiseAll } from './to-promise-all'
import { noShorthand } from './no-shorthand'
import { noType } from './no-type'
import { keepUnique } from './keep-unique'
import { regex101 } from './regex101'

// @keep-sorted
export {
Expand All @@ -20,6 +21,7 @@ export {
keepUnique,
noShorthand,
noType,
regex101,
toArrow,
toDestructuring,
toDynamicImport,
Expand All @@ -38,6 +40,7 @@ export const builtinCommands = [
keepUnique,
noShorthand,
noType,
regex101,
toArrow,
toDestructuring,
toDynamicImport,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/inline-arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'

export const inlineArrow: Command = {
name: 'inline-arrow',
match: /^[\/:@]\s*(inline-arrow|ia)$/,
match: /^\s*[\/:@]\s*(inline-arrow|ia)$/,
action(ctx) {
const arrowFn = ctx.findNodeBelow('ArrowFunctionExpression')
if (!arrowFn)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/no-shorthand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Command } from '../types'

export const noShorthand: Command = {
name: 'no-shorthand',
match: /^[\/:@]\s*(no-shorthand|nsh)$/,
match: /^\s*[\/:@]\s*(no-shorthand|nsh)$/,
action(ctx) {
const nodes = ctx.findNodeBelow<AST_NODE_TYPES.Property>({
filter: node => node.type === 'Property' && node.shorthand,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/no-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from '../types'

export const noType: Command = {
name: 'no-type',
match: /^[\/:@]\s*(no-type|nt)$/,
match: /^\s*[\/:@]\s*(no-type|nt)$/,
action(ctx) {
const nodes = ctx.findNodeBelow({
filter: node => node.type.startsWith('TS'),
Expand Down
70 changes: 70 additions & 0 deletions src/commands/regex101.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# `regex101`

Generate up-to-date [regex101](https://regex101.com/) links for your RegExp patterns in jsdoc comments. Helps you test and inspect the RegExp easily.

## Triggers

- `// @regex101`
- `/* @regex101 */`

## Examples

```js
/**
* RegExp to match foo or bar, optionally wrapped in quotes.
*
* @regex101
*/
const foo = /(['"])?(foo|bar)\\1?/gi
```

Will be updated to:

```js
/**
* RegExp to match foo or bar, optionally wrapped in quotes.
*
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript
*/
const foo = /(['"])?(foo|bar)\\1?/gi
```

An whenever you update the RegExp pattern, the link will be updated as well.

### Optional Test Strings

Test string can also be provided via an optional `@example` tag:

```js
/**
* Some jsdoc
*
* @example str
* \`\`\`js
* if ('foo'.match(foo)) {
* const foo = bar
* }
* \`\`\`
*
* @regex101
*/
const foo = /(['"])?(foo|bar)\\1?/gi
```

Will be updated to:

```js
/**
* Some jsdoc
*
* @example str
* \`\`\`js
* if ('foo'.match(foo)) {
* const foo = bar
* }
* \`\`\`
*
* @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
*/
const foo = /(['"])?(foo|bar)\\1?/gi
```
74 changes: 74 additions & 0 deletions src/commands/regex101.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { regex101 as command } from './regex101'
import { $, run } from './_test-utils'

run(
command,
// basic
{
code: $`
// @regex101
const foo = /(?:\\b|\\s)@regex101(\\s[^\\s]+)?(?:\\s|\\b|$)/g
`,
output: output => expect(output).toMatchInlineSnapshot(`
"// @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
const foo = /(?:\\b|\\s)@regex101(\\s[^\\s]+)?(?:\\s|\\b|$)/g"
`),
errors: ['command-fix'],
},
// block comment
{
code: $`
/**
* Some jsdoc
*
* @regex101
* @deprecated
*/
const foo = /(['"])?(foo|bar)\\1?/gi
`,
output: output => expect(output).toMatchInlineSnapshot(`
"/**
* Some jsdoc
*
* @regex101 https://regex101.com/?regex=%28%5B%27%22%5D%29%3F%28foo%7Cbar%29%5C1%3F&flags=gi&flavor=javascript
* @deprecated
*/
const foo = /(['"])?(foo|bar)\\1?/gi"
`),
errors: ['command-fix'],
},
// example block
{
code: $`
/**
* Some jsdoc
*
* @example str
* \`\`\`js
* if ('foo'.match(foo)) {
* const foo = bar
* }
* \`\`\`
*
* @regex101
*/
const foo = /(['"])?(foo|bar)\\1?/gi
`,
output: output => expect(output).toMatchInlineSnapshot(`
"/**
* Some jsdoc
*
* @example str
* \`\`\`js
* if ('foo'.match(foo)) {
* const foo = bar
* }
* \`\`\`
*
* @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
*/
const foo = /(['"])?(foo|bar)\\1?/gi"
`),
errors: ['command-fix'],
},
)
71 changes: 71 additions & 0 deletions src/commands/regex101.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { parseComment } from '@es-joy/jsdoccomment'
import type { Command, Tree } from '../types'

// @regex101 https://regex101.com/?regex=%60%60%60%28.*%29%5Cn%28%5B%5Cs%5CS%5D*%29%5Cn%60%60%60&flavor=javascript
const reCodeBlock = /```(.*)\n([\s\S]*)\n```/

export const regex101: Command = {
name: 'regex101',
/**
* @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
*/
match: /(\b|\s|^)(@regex101)(\s[^\s]+)?(\b|\s|$)/,
commentType: 'both',
action(ctx) {
const literal = ctx.findNodeBelow((n) => {
return n.type === 'Literal' && 'regex' in n
}) as Tree.RegExpLiteral | undefined
if (!literal)
return ctx.reportError('Unable to find arrow function to convert')

const [
_fullStr = '',
spaceBefore = '',
commandStr = '',
existingUrl = '',
_spaceAfter = '',
] = ctx.matches as string[]

let example: string | undefined

if (ctx.comment.value.includes('```') && ctx.comment.value.includes('@example')) {
try {
const parsed = parseComment(ctx.comment, '')
const tag = parsed.tags.find(t => t.tag === 'example')
const description = tag?.description
const code = description?.match(reCodeBlock)?.[2].trim()
if (code)
example = code
}
catch (e) {}
}

// docs: https://github.com/firasdib/Regex101/wiki/FAQ#how-to-prefill-the-fields-on-the-interface-via-url
const query = new URLSearchParams()
query.set('regex', literal.regex.pattern)
if (literal.regex.flags)
query.set('flags', literal.regex.flags)
query.set('flavor', 'javascript')
if (example)
query.set('testString', example)
const url = `https://regex101.com/?${query}`

if (existingUrl.trim() === url.trim())
return

const indexStart = ctx.comment.range[0] + ctx.matches.index! + spaceBefore.length + 2 /** comment prefix */
const indexEnd = indexStart + commandStr.length + existingUrl.length

ctx.report({
loc: {
start: ctx.source.getLocFromIndex(indexStart),
end: ctx.source.getLocFromIndex(indexEnd),
},
removeComment: false,
message: `Update the regex101 link`,
fix(fixer) {
return fixer.replaceTextRange([indexStart, indexEnd], `@regex101 ${url}`)
},
})
},
}
2 changes: 1 addition & 1 deletion src/commands/to-arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from '../types'

export const toArrow: Command = {
name: 'to-arrow',
match: /^[\/:@]\s*(to-arrow|2a|ta)$/,
match: /^\s*[\/:@]\s*(to-arrow|2a|ta)$/,
action(ctx) {
const fn = ctx.findNodeBelow('FunctionDeclaration', 'FunctionExpression')
if (!fn)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-destructuring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from '../types'

export const toDestructuring: Command = {
name: 'to-destructuring',
match: /^[\/:@]\s*(?:to-|2)?(?:destructuring|dest)?$/i,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:destructuring|dest)?$/i,
action(ctx) {
const node = ctx.findNodeBelow(
'VariableDeclaration',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-dynamic-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'

export const toDynamicImport: Command = {
name: 'to-dynamic-import',
match: /^[\/:@]\s*(?:to-|2)?(?:dynamic|d)(?:-?import)?$/i,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:dynamic|d)(?:-?import)?$/i,
action(ctx) {
const node = ctx.findNodeBelow('ImportDeclaration')
if (!node)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-for-each.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const FOR_TRAVERSE_IGNORE: NodeType[] = [

export const toForEach: Command = {
name: 'to-for-each',
match: /^[\/:@]\s*(?:to-|2)?(?:for-?each)$/i,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:for-?each)$/i,
action(ctx) {
const node = ctx.findNodeBelow('ForInStatement', 'ForOfStatement')
if (!node)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-for-of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FOR_TRAVERSE_IGNORE } from './to-for-each'

export const toForOf: Command = {
name: 'to-for-of',
match: /^[\/:@]\s*(?:to-|2)?(?:for-?of)$/i,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:for-?of)$/i,
action(ctx) {
const target = ctx.findNodeBelow((node) => {
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'forEach')
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command, Tree } from '../types'

export const toFunction: Command = {
name: 'to-function',
match: /^[\/:@]\s*(to-(?:fn|function)|2f|tf)$/,
match: /^\s*[\/:@]\s*(to-(?:fn|function)|2f|tf)$/,
action(ctx) {
const arrowFn = ctx.findNodeBelow('ArrowFunctionExpression')
if (!arrowFn)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-string-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getNodesByIndexes, parseToNumberArray } from './_utils'

export const toStringLiteral: Command = {
name: 'to-string-literal',
match: /^[\/:@]\s*(?:to-|2)?(?:string-literal|sl)\s{0,}(.*)?$/,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:string-literal|sl)\s{0,}(.*)?$/,
action(ctx) {
const numbers = ctx.matches[1]
// From integers 1-based to 0-based to match array indexes
Expand Down
2 changes: 1 addition & 1 deletion src/commands/to-template-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type NodeTypes = Tree.StringLiteral | Tree.BinaryExpression

export const toTemplateLiteral: Command = {
name: 'to-template-literal',
match: /^[\/:@]\s*(?:to-|2)?(?:template-literal|tl)\s{0,}(.*)?$/,
match: /^\s*[\/:@]\s*(?:to-|2)?(?:template-literal|tl)\s{0,}(.*)?$/,
action(ctx) {
const numbers = ctx.matches[1]
// From integers 1-based to 0-based to match array indexes
Expand Down
2 changes: 1 addition & 1 deletion src/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function createRuleWithCommands(commands: Command[]) {
const comments = sc.getAllComments()

for (const comment of comments) {
const commandRaw = comment.value.trim()
const commandRaw = comment.value
for (const command of commands) {
const type = command.commentType ?? 'line'
if (type === 'line' && comment.type !== 'Line')
Expand Down

0 comments on commit 6e75efc

Please sign in to comment.