From 6e75efc07a3c048fff5c3146cbb30204e8bbcaab Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 8 May 2024 18:01:25 +0200 Subject: [PATCH] feat(regex101): new command --- package.json | 3 ++ pnpm-lock.yaml | 17 +++++++ src/commands/index.ts | 3 ++ src/commands/inline-arrow.ts | 2 +- src/commands/no-shorthand.ts | 2 +- src/commands/no-type.ts | 2 +- src/commands/regex101.md | 70 +++++++++++++++++++++++++++ src/commands/regex101.test.ts | 74 +++++++++++++++++++++++++++++ src/commands/regex101.ts | 71 +++++++++++++++++++++++++++ src/commands/to-arrow.ts | 2 +- src/commands/to-destructuring.ts | 2 +- src/commands/to-dynamic-import.ts | 2 +- src/commands/to-for-each.ts | 2 +- src/commands/to-for-of.ts | 2 +- src/commands/to-function.ts | 2 +- src/commands/to-string-literal.ts | 2 +- src/commands/to-template-literal.ts | 2 +- src/rule.ts | 2 +- 18 files changed, 250 insertions(+), 12 deletions(-) create mode 100644 src/commands/regex101.md create mode 100644 src/commands/regex101.test.ts create mode 100644 src/commands/regex101.ts diff --git a/package.json b/package.json index 13fb912..42a0965 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa4eb08..cf1ff00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@es-joy/jsdoccomment': + specifier: ^0.43.0 + version: 0.43.0 devDependencies: '@antfu/eslint-config': specifier: ^2.17.0 @@ -530,6 +534,10 @@ packages: resolution: {integrity: sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==} engines: {node: '>=16'} + '@es-joy/jsdoccomment@0.43.0': + resolution: {integrity: sha512-Q1CnsQrytI3TlCB1IVWXWeqUIPGVEKGaE7IbVdt13Nq/3i0JESAkQQERrfiQkmlpijl+++qyqPgaS31Bvc1jRQ==} + engines: {node: '>=16'} + '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -4209,6 +4217,15 @@ snapshots: esquery: 1.5.0 jsdoc-type-pratt-parser: 4.0.0 + '@es-joy/jsdoccomment@0.43.0': + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.5 + '@typescript-eslint/types': 7.8.0 + comment-parser: 1.4.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + '@esbuild/aix-ppc64@0.19.12': optional: true diff --git a/src/commands/index.ts b/src/commands/index.ts index 22a2894..eec14a3 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -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 { @@ -20,6 +21,7 @@ export { keepUnique, noShorthand, noType, + regex101, toArrow, toDestructuring, toDynamicImport, @@ -38,6 +40,7 @@ export const builtinCommands = [ keepUnique, noShorthand, noType, + regex101, toArrow, toDestructuring, toDynamicImport, diff --git a/src/commands/inline-arrow.ts b/src/commands/inline-arrow.ts index ae7101a..9d6c7ac 100644 --- a/src/commands/inline-arrow.ts +++ b/src/commands/inline-arrow.ts @@ -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) diff --git a/src/commands/no-shorthand.ts b/src/commands/no-shorthand.ts index 9efac97..fd43167 100644 --- a/src/commands/no-shorthand.ts +++ b/src/commands/no-shorthand.ts @@ -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({ filter: node => node.type === 'Property' && node.shorthand, diff --git a/src/commands/no-type.ts b/src/commands/no-type.ts index f4589ed..76c9757 100644 --- a/src/commands/no-type.ts +++ b/src/commands/no-type.ts @@ -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'), diff --git a/src/commands/regex101.md b/src/commands/regex101.md new file mode 100644 index 0000000..1dda4e7 --- /dev/null +++ b/src/commands/regex101.md @@ -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 +``` diff --git a/src/commands/regex101.test.ts b/src/commands/regex101.test.ts new file mode 100644 index 0000000..d3f6cb2 --- /dev/null +++ b/src/commands/regex101.test.ts @@ -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'], + }, +) diff --git a/src/commands/regex101.ts b/src/commands/regex101.ts new file mode 100644 index 0000000..40d388a --- /dev/null +++ b/src/commands/regex101.ts @@ -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}`) + }, + }) + }, +} diff --git a/src/commands/to-arrow.ts b/src/commands/to-arrow.ts index 1c0ef94..e8f7dd8 100644 --- a/src/commands/to-arrow.ts +++ b/src/commands/to-arrow.ts @@ -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) diff --git a/src/commands/to-destructuring.ts b/src/commands/to-destructuring.ts index b0077cd..7855290 100644 --- a/src/commands/to-destructuring.ts +++ b/src/commands/to-destructuring.ts @@ -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', diff --git a/src/commands/to-dynamic-import.ts b/src/commands/to-dynamic-import.ts index 5f31233..abca66a 100644 --- a/src/commands/to-dynamic-import.ts +++ b/src/commands/to-dynamic-import.ts @@ -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) diff --git a/src/commands/to-for-each.ts b/src/commands/to-for-each.ts index 29d1b3f..111403a 100644 --- a/src/commands/to-for-each.ts +++ b/src/commands/to-for-each.ts @@ -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) diff --git a/src/commands/to-for-of.ts b/src/commands/to-for-of.ts index 46cca36..2204876 100644 --- a/src/commands/to-for-of.ts +++ b/src/commands/to-for-of.ts @@ -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') diff --git a/src/commands/to-function.ts b/src/commands/to-function.ts index 65f758b..811cf38 100644 --- a/src/commands/to-function.ts +++ b/src/commands/to-function.ts @@ -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) diff --git a/src/commands/to-string-literal.ts b/src/commands/to-string-literal.ts index a945f6f..af57ab6 100644 --- a/src/commands/to-string-literal.ts +++ b/src/commands/to-string-literal.ts @@ -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 diff --git a/src/commands/to-template-literal.ts b/src/commands/to-template-literal.ts index bcb23db..d2784d8 100644 --- a/src/commands/to-template-literal.ts +++ b/src/commands/to-template-literal.ts @@ -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 diff --git a/src/rule.ts b/src/rule.ts index 3f5e46f..9fe39ec 100644 --- a/src/rule.ts +++ b/src/rule.ts @@ -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')