From f824823ae23ef1097677bdb527591a8c576dd0da Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Tue, 1 Jul 2025 18:14:18 +0200 Subject: [PATCH 1/4] refactor: use regexp-groups to simplify ignores (POC) --- lib/rules/syntaxes/slot-attribute.js | 14 +++---- lib/utils/regexp.js | 26 +++++++++++- tests/lib/utils/regexp.js | 60 +++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js index b77fc6c20..2b11d9910 100644 --- a/lib/rules/syntaxes/slot-attribute.js +++ b/lib/rules/syntaxes/slot-attribute.js @@ -16,8 +16,7 @@ module.exports = { /** @type {{ ignore: string[] }} */ const options = context.options[0] || {} const { ignore = [] } = options - /** @type {RegExp[]} */ - const ignorePatterns = ignore.map(regexp.toRegExp) + const ignoreGroupMatcher = regexp.toRegExpGroupMatcher(ignore) const sourceCode = context.getSourceCode() const tokenStore = @@ -125,15 +124,12 @@ module.exports = { */ function reportSlot(slotAttr) { const componentName = slotAttr.parent.parent.rawName - const componentNamePascalCase = casing.pascalCase(componentName) - const componentNameKebabCase = casing.kebabCase(componentName) if ( - ignorePatterns.some( - (pattern) => - pattern.test(componentName) || - pattern.test(componentNamePascalCase) || - pattern.test(componentNameKebabCase) + ignoreGroupMatcher( + componentName, + casing.pascalCase(componentName), + casing.kebabCase(componentName) ) ) { return diff --git a/lib/utils/regexp.js b/lib/utils/regexp.js index 3ee40ae41..4eb84b34f 100644 --- a/lib/utils/regexp.js +++ b/lib/utils/regexp.js @@ -41,8 +41,32 @@ function isRegExp(string) { return RE_REGEXP_STR.test(string) } +/** + * Converts an array of strings to a singular function to match any of them. + * This function converts each string to a `RegExp` and returns a function that checks all of them. + * + * @param {string[]} [patterns] The strings or regular expression strings to match. + * @returns {(...toCheck: string[]) => boolean} Returns a function that checks if any string matches any of the given patterns. + */ +function toRegExpGroupMatcher(patterns = []) { + if (patterns.length === 0) { + return () => false + } + + // In the future, we could optimize this by joining expressions with identical flags. + const regexps = patterns.map(toRegExp) + + if (regexps.length === 1) { + return (...toCheck) => toCheck.some((str) => regexps[0].test(str)) + } + + return (...toCheck) => + regexps.some((regexp) => toCheck.some((str) => regexp.test(str))) +} + module.exports = { escape, toRegExp, - isRegExp + isRegExp, + toRegExpGroupMatcher } diff --git a/tests/lib/utils/regexp.js b/tests/lib/utils/regexp.js index 830fa2a11..5ba37a36e 100644 --- a/tests/lib/utils/regexp.js +++ b/tests/lib/utils/regexp.js @@ -1,6 +1,10 @@ 'use strict' -const { escape, toRegExp } = require('../../../lib/utils/regexp') +const { + escape, + toRegExp, + toRegExpGroupMatcher +} = require('../../../lib/utils/regexp') const assert = require('assert') const ESCAPED = '\\^\\$\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|\\\\' @@ -36,3 +40,57 @@ describe('toRegExp()', () => { assert.deepEqual(toRegExp(`${/[\sA-Z]+/u}`), /[\sA-Z]+/u) }) }) + +describe('toRegExpCheckGroup()', () => { + it('should return a function missing inout', () => { + const groupMatcher = toRegExpGroupMatcher() + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), false) + assert.strictEqual(groupMatcher('bar'), false) + }) + + it('should return a function for empty array', () => { + const groupMatcher = toRegExpGroupMatcher([]) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), false) + assert.strictEqual(groupMatcher('bar'), false) + }) + + it('should return a function for single simple pattern', () => { + const groupMatcher = toRegExpGroupMatcher(['foo']) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('foo', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('foobar'), false) + assert.strictEqual(groupMatcher('afoo', 'fooa', 'afooa', 'bar'), false) + }) + + it('should return a function for multiple simple patterns', () => { + const groupMatcher = toRegExpGroupMatcher(['foo', 'bar']) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('bar', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('foobar'), false) + assert.strictEqual(groupMatcher('afoo', 'fooa', 'afooa'), false) + }) + + it('should return a function for single regexp pattern', () => { + const groupMatcher = toRegExpGroupMatcher(['/^foo/']) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('fooa', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'fooa'), true) + assert.strictEqual(groupMatcher('barfoo'), false) + assert.strictEqual(groupMatcher('afoo', 'afooa', 'bar'), false) + }) + + it('should return a function for multiple regexp patterns', () => { + const groupMatcher = toRegExpGroupMatcher(['/^foo/', '/bar$/']) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('bar', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('barfoo'), false) + assert.strictEqual(groupMatcher('afoo', 'afooa', 'bara'), false) + }) +}) From 8570e6c9ad0f95696f9f7d47813e1f608267075f Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Wed, 2 Jul 2025 16:09:55 +0200 Subject: [PATCH 2/4] chore: rename --- lib/rules/syntaxes/slot-attribute.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js index 2b11d9910..8d854230c 100644 --- a/lib/rules/syntaxes/slot-attribute.js +++ b/lib/rules/syntaxes/slot-attribute.js @@ -16,7 +16,7 @@ module.exports = { /** @type {{ ignore: string[] }} */ const options = context.options[0] || {} const { ignore = [] } = options - const ignoreGroupMatcher = regexp.toRegExpGroupMatcher(ignore) + const isAnyIgnored = regexp.toRegExpGroupMatcher(ignore) const sourceCode = context.getSourceCode() const tokenStore = @@ -126,7 +126,7 @@ module.exports = { const componentName = slotAttr.parent.parent.rawName if ( - ignoreGroupMatcher( + isAnyIgnored( componentName, casing.pascalCase(componentName), casing.kebabCase(componentName) From 2dedddb555304f12922dbf66871aa158a24716f4 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Wed, 2 Jul 2025 16:11:02 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Flo Edelmann --- tests/lib/utils/regexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/utils/regexp.js b/tests/lib/utils/regexp.js index 5ba37a36e..585e054c6 100644 --- a/tests/lib/utils/regexp.js +++ b/tests/lib/utils/regexp.js @@ -42,7 +42,7 @@ describe('toRegExp()', () => { }) describe('toRegExpCheckGroup()', () => { - it('should return a function missing inout', () => { + it('should return a function missing input', () => { const groupMatcher = toRegExpGroupMatcher() assert.strictEqual(groupMatcher(''), false) assert.strictEqual(groupMatcher('foo'), false) From dd474bb5ac773dcf25cbec27a99c41311c841cf2 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 3 Jul 2025 22:56:55 +0200 Subject: [PATCH 4/4] refactor: replace regexp loops with regexpGroupMatchers --- lib/rules/attribute-hyphenation.js | 15 +--- .../component-name-in-template-casing.js | 7 +- lib/rules/custom-event-name-casing.js | 7 +- lib/rules/no-restricted-block.js | 18 +---- lib/rules/no-restricted-class.js | 28 ++------ lib/rules/no-restricted-component-names.js | 2 +- lib/rules/no-restricted-component-options.js | 18 +---- lib/rules/no-restricted-custom-event.js | 18 +---- lib/rules/no-restricted-props.js | 18 +---- lib/rules/no-restricted-static-attribute.js | 26 ++----- lib/rules/no-restricted-v-bind.js | 22 ++---- lib/rules/no-restricted-v-on.js | 23 ++----- lib/rules/no-undef-properties.js | 9 ++- lib/rules/prefer-true-attribute-shorthand.js | 7 +- lib/rules/prop-name-casing.js | 9 +-- lib/rules/restricted-component-names.js | 7 +- lib/rules/v-on-event-hyphenation.js | 15 +--- lib/utils/regexp.js | 19 ++++-- tests/lib/utils/regexp.js | 68 ++++++++++++++++++- 19 files changed, 137 insertions(+), 199 deletions(-) diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js index 65d096cd4..2eea5b8a5 100644 --- a/lib/rules/attribute-hyphenation.js +++ b/lib/rules/attribute-hyphenation.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const svgAttributes = require('../utils/svg-attributes-weird-case.json') /** @@ -79,11 +79,7 @@ module.exports = { const option = context.options[0] const optionsPayload = context.options[1] const useHyphenated = option !== 'never' - /** @type {RegExp[]} */ - const ignoredTagsRegexps = ( - (optionsPayload && optionsPayload.ignoreTags) || - [] - ).map(toRegExp) + const ignoredTagsMatcher = toRegExpGroupMatcher(optionsPayload?.ignoreTags) const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes] if (optionsPayload && optionsPayload.ignore) { @@ -142,17 +138,12 @@ module.exports = { return useHyphenated ? value.toLowerCase() === value : !/-/.test(value) } - /** @param {string} name */ - function isIgnoredTagName(name) { - return ignoredTagsRegexps.some((re) => re.test(name)) - } - return utils.defineTemplateBodyVisitor(context, { VAttribute(node) { const element = node.parent.parent if ( (!utils.isCustomComponent(element) && element.name !== 'slot') || - isIgnoredTagName(element.rawName) + ignoredTagsMatcher(element.rawName) ) return diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js index d330f60da..3e359d23a 100644 --- a/lib/rules/component-name-in-template-casing.js +++ b/lib/rules/component-name-in-template-casing.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const allowedCaseOptions = ['PascalCase', 'kebab-case'] const defaultCase = 'PascalCase' @@ -81,8 +81,7 @@ module.exports = { const caseType = allowedCaseOptions.includes(caseOption) ? caseOption : defaultCase - /** @type {RegExp[]} */ - const ignores = (options.ignores || []).map(toRegExp) + const ignoreMatcher = toRegExpGroupMatcher(options.ignores) /** @type {string[]} */ const globals = (options.globals || []).map(casing.pascalCase) const registeredComponentsOnly = options.registeredComponentsOnly !== false @@ -116,7 +115,7 @@ module.exports = { * @returns {boolean} `true` if the given node is the verification target node. */ function isVerifyTarget(node) { - if (ignores.some((re) => re.test(node.rawName))) { + if (ignoreMatcher(node.rawName)) { // ignore return false } diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index aff4609b5..9de00b3dd 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -7,7 +7,7 @@ const { findVariable } = require('@eslint-community/eslint-utils') const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') /** * @typedef {import('../utils').VueObjectData} VueObjectData @@ -92,8 +92,7 @@ module.exports = { const caseType = context.options[0] || DEFAULT_CASE const objectOption = context.options[1] || {} const caseChecker = casing.getChecker(caseType) - /** @type {RegExp[]} */ - const ignores = (objectOption.ignores || []).map(toRegExp) + const ignoreMatcher = toRegExpGroupMatcher(objectOption.ignores) /** * Check whether the given event name is valid. @@ -109,7 +108,7 @@ module.exports = { */ function verify(nameWithLoc) { const name = nameWithLoc.name - if (isValidEventName(name) || ignores.some((re) => re.test(name))) { + if (isValidEventName(name) || ignoreMatcher(name)) { return } context.report({ diff --git a/lib/rules/no-restricted-block.js b/lib/rules/no-restricted-block.js index 87b4bda3e..000e2c7b8 100644 --- a/lib/rules/no-restricted-block.js +++ b/lib/rules/no-restricted-block.js @@ -12,30 +12,16 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(block) { - return matcher(block.rawName) + return matcher.test(block.rawName) } } } diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js index 41d30df2d..df0203435 100644 --- a/lib/rules/no-restricted-class.js +++ b/lib/rules/no-restricted-class.js @@ -12,20 +12,15 @@ const regexp = require('../utils/regexp') * @param {string} className * @param {*} node * @param {RuleContext} context - * @param {Set} forbiddenClasses - * @param {Array} forbiddenClassesRegexps + * @param {(name: string) => boolean} forbiddenGroupMatcher */ const reportForbiddenClass = ( className, node, context, - forbiddenClasses, - forbiddenClassesRegexps + forbiddenGroupMatcher ) => { - if ( - forbiddenClasses.has(className) || - forbiddenClassesRegexps.some((re) => re.test(className)) - ) { + if (forbiddenGroupMatcher(className)) { const loc = node.value ? node.value.loc : node.loc context.report({ node, @@ -123,10 +118,8 @@ module.exports = { /** @param {RuleContext} context */ create(context) { - const forbiddenClasses = new Set(context.options || []) - const forbiddenClassesRegexps = (context.options || []) - .filter((cl) => regexp.isRegExp(cl)) - .map((cl) => regexp.toRegExp(cl)) + const { options = [] } = context + const forbiddenGroupMatcher = regexp.toRegExpGroupMatcher(options) return utils.defineTemplateBodyVisitor(context, { /** @@ -134,13 +127,7 @@ module.exports = { */ 'VAttribute[directive=false][key.name="class"][value!=null]'(node) { for (const className of node.value.value.split(/\s+/)) { - reportForbiddenClass( - className, - node, - context, - forbiddenClasses, - forbiddenClassesRegexps - ) + reportForbiddenClass(className, node, context, forbiddenGroupMatcher) } }, @@ -159,8 +146,7 @@ module.exports = { className, reportNode, context, - forbiddenClasses, - forbiddenClassesRegexps + forbiddenGroupMatcher ) } } diff --git a/lib/rules/no-restricted-component-names.js b/lib/rules/no-restricted-component-names.js index e5111a748..df5ad4a23 100644 --- a/lib/rules/no-restricted-component-names.js +++ b/lib/rules/no-restricted-component-names.js @@ -22,7 +22,7 @@ const { isRegExp, toRegExp } = require('../utils/regexp') */ function buildMatcher(str) { if (isRegExp(str)) { - const regex = toRegExp(str) + const regex = toRegExp(str, { remove: 'g' }) return (s) => regex.test(s) } return (s) => s === casing.pascalCase(str) || s === casing.kebabCase(str) diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js index b8563a92b..227a89382 100644 --- a/lib/rules/no-restricted-component-options.js +++ b/lib/rules/no-restricted-component-options.js @@ -23,21 +23,6 @@ const regexp = require('../utils/regexp') * @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester */ -/** - * @param {string} str - * @returns {Matcher} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} - /** * @param {string | string[] | { name: string | string[], message?: string } } option * @returns {ParsedOption} @@ -65,7 +50,8 @@ function parseOption(option) { if (name === '*') { steps.push({ wildcard: true }) } else { - steps.push({ test: buildMatcher(name) }) + const matcher = regexp.toRegExp(name, { remove: 'g' }) + steps.push({ test: (value) => matcher.test(value) }) } } const message = option.message diff --git a/lib/rules/no-restricted-custom-event.js b/lib/rules/no-restricted-custom-event.js index 5ddda037f..93c1aa764 100644 --- a/lib/rules/no-restricted-custom-event.js +++ b/lib/rules/no-restricted-custom-event.js @@ -15,30 +15,16 @@ const regexp = require('../utils/regexp') * @property {string|undefined} [suggest] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {string|{event: string, message?: string, suggest?: string}} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(name) { - return matcher(name) + return matcher.test(name) } } } diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js index 2d2f74bb0..e0684393d 100644 --- a/lib/rules/no-restricted-props.js +++ b/lib/rules/no-restricted-props.js @@ -18,30 +18,16 @@ const regexp = require('../utils/regexp') * @property {string|undefined} [suggest] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {string|{name:string, message?: string, suggest?:string}} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(name) { - return matcher(name) + return matcher.test(name) } } } diff --git a/lib/rules/no-restricted-static-attribute.js b/lib/rules/no-restricted-static-attribute.js index d9241620b..d7223044a 100644 --- a/lib/rules/no-restricted-static-attribute.js +++ b/lib/rules/no-restricted-static-attribute.js @@ -15,30 +15,16 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test({ key }) { - return matcher(key.rawName) + return matcher.test(key.rawName) } } } @@ -53,25 +39,25 @@ function parseOption(option) { return node.value == null || node.value.value === node.key.rawName } } else { - const valueMatcher = buildMatcher(option.value) + const valueMatcher = regexp.toRegExp(option.value, { remove: 'g' }) parsed.test = (node) => { if (!keyTest(node)) { return false } - return node.value != null && valueMatcher(node.value.value) + return node.value != null && valueMatcher.test(node.value.value) } } parsed.useValue = true } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (node) => { if (!argTest(node)) { return false } const element = node.parent.parent - return tagMatcher(element.rawName) + return tagMatcher.test(element.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js index f9bf5462b..e16622174 100644 --- a/lib/rules/no-restricted-v-bind.js +++ b/lib/rules/no-restricted-v-bind.js @@ -22,33 +22,19 @@ const DEFAULT_OPTIONS = [ } ] -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(key) { return Boolean( key.argument && key.argument.type === 'VIdentifier' && - matcher(key.argument.rawName) + matcher.test(key.argument.rawName) ) }, modifiers: [] @@ -77,13 +63,13 @@ function parseOption(option) { } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (key) => { if (!argTest(key)) { return false } const element = key.parent.parent.parent - return tagMatcher(element.rawName) + return tagMatcher.test(element.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index 2379df349..893d511a6 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -15,34 +15,19 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} - /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(key) { return Boolean( key.argument && key.argument.type === 'VIdentifier' && - matcher(key.argument.rawName) + matcher.test(key.argument.rawName) ) } } @@ -70,12 +55,12 @@ function parseOption(option) { } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (key) => { if (!argTest(key)) { return false } - return tagMatcher(key.parent.parent.parent.rawName) + return tagMatcher.test(key.parent.parent.parent.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js index 711c2ed22..53d2dcf37 100644 --- a/lib/rules/no-undef-properties.js +++ b/lib/rules/no-undef-properties.js @@ -6,7 +6,7 @@ const utils = require('../utils') const reserved = require('../utils/vue-reserved.json') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const { getStyleVariablesContext } = require('../utils/style-variables') const { definePropertyReferenceExtractor @@ -106,9 +106,8 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} - const ignores = /** @type {string[]} */ ( - options.ignores || [String.raw`/^\$/`] - ).map(toRegExp) + const { ignores = [String.raw`/^\$/`] } = options + const ignoreMatcher = toRegExpGroupMatcher(ignores) const propertyReferenceExtractor = definePropertyReferenceExtractor(context) const programNode = context.getSourceCode().ast /** @@ -190,7 +189,7 @@ module.exports = { report(node, name, messageId = 'undef') { if ( reserved.includes(name) || - ignores.some((ignore) => ignore.test(name)) || + ignoreMatcher(name) || propertiesDefinedByStoreHelpers.has(name) ) { return diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js index 817525d1d..3f5446bda 100644 --- a/lib/rules/prefer-true-attribute-shorthand.js +++ b/lib/rules/prefer-true-attribute-shorthand.js @@ -4,7 +4,7 @@ */ 'use strict' -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const utils = require('../utils') /** @@ -99,8 +99,7 @@ module.exports = { create(context) { /** @type {'always' | 'never'} */ const option = context.options[0] || 'always' - /** @type {RegExp[]} */ - const exceptReg = (context.options[1]?.except || []).map(toRegExp) + const exceptMatcher = toRegExpGroupMatcher(context.options[1]?.except) /** * @param {VAttribute | VDirective} node @@ -155,7 +154,7 @@ module.exports = { const name = getAttributeName(node) if (name === null) return - const isExcepted = exceptReg.some((re) => re.test(name)) + const isExcepted = exceptMatcher(name) if (shouldConvertToLongForm(node, isExcepted, option)) { const key = /** @type {VIdentifier} */ (node.key) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index fd4f0dc31..afd59e79d 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const allowedCaseOptions = ['camelCase', 'snake_case'] /** @@ -16,8 +16,9 @@ const allowedCaseOptions = ['camelCase', 'snake_case'] /** @param {RuleContext} context */ function create(context) { const options = context.options[0] - /** @type {RegExp[]} */ - const ignoreProps = (context.options[1]?.ignoreProps || []).map(toRegExp) + const ignorePropsMatcher = toRegExpGroupMatcher( + context.options[1]?.ignoreProps + ) const caseType = allowedCaseOptions.includes(options) ? options : 'camelCase' const checker = casing.getChecker(caseType) @@ -30,7 +31,7 @@ function create(context) { if (propName == null) { continue } - if (!checker(propName) && !ignoreProps.some((re) => re.test(propName))) { + if (!checker(propName) && !ignorePropsMatcher(propName)) { context.report({ node: item.node, messageId: 'invalidCase', diff --git a/lib/rules/restricted-component-names.js b/lib/rules/restricted-component-names.js index 636224db6..9ba40d6a2 100644 --- a/lib/rules/restricted-component-names.js +++ b/lib/rules/restricted-component-names.js @@ -5,7 +5,7 @@ 'use strict' const utils = require('../utils') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const htmlElements = require('../utils/html-elements.json') const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json') @@ -51,12 +51,11 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} - /** @type {RegExp[]} */ - const allow = (options.allow || []).map(toRegExp) + const allowMatcher = toRegExpGroupMatcher(options.allow) /** @param {string} name */ function isAllowedTarget(name) { - return reservedNames.has(name) || allow.some((re) => re.test(name)) + return reservedNames.has(name) || allowMatcher(name) } return utils.defineTemplateBodyVisitor(context, { diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js index c9fac76e8..11360882c 100644 --- a/lib/rules/v-on-event-hyphenation.js +++ b/lib/rules/v-on-event-hyphenation.js @@ -2,7 +2,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') module.exports = { meta: { @@ -63,11 +63,7 @@ module.exports = { const useHyphenated = option !== 'never' /** @type {string[]} */ const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || [] - /** @type {RegExp[]} */ - const ignoredTagsRegexps = ( - (optionsPayload && optionsPayload.ignoreTags) || - [] - ).map(toRegExp) + const ignoredTagsMatcher = toRegExpGroupMatcher(optionsPayload?.ignoreTags) const autofix = Boolean(optionsPayload && optionsPayload.autofix) const caseConverter = casing.getConverter( @@ -111,17 +107,12 @@ module.exports = { return useHyphenated ? value.toLowerCase() === value : !/-/.test(value) } - /** @param {string} name */ - function isIgnoredTagName(name) { - return ignoredTagsRegexps.some((re) => re.test(name)) - } - return utils.defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name.name='on']"(node) { const element = node.parent.parent if ( !utils.isCustomComponent(element) || - isIgnoredTagName(element.rawName) + ignoredTagsMatcher(element.rawName) ) { return } diff --git a/lib/utils/regexp.js b/lib/utils/regexp.js index 4eb84b34f..7e8b9fff9 100644 --- a/lib/utils/regexp.js +++ b/lib/utils/regexp.js @@ -22,14 +22,25 @@ function escape(string) { * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`. * * @param {string} string The string to convert. + * @param {{add?: string, remove?: string}} [flags] The flags to add or remove. + * - `add`: Flags to add to the `RegExp` (e.g. `'i'` for case-insensitive). + * - `remove`: Flags to remove from the `RegExp` (e.g. `'g'` to remove global matching). * @returns {RegExp} Returns the `RegExp`. */ -function toRegExp(string) { +function toRegExp(string, flags = {}) { const parts = RE_REGEXP_STR.exec(string) + const { add: forceAddFlags = '', remove: forceRemoveFlags = '' } = + typeof flags === 'object' ? flags : {} // Avoid issues when this is called diretly from array.map if (parts) { - return new RegExp(parts[1], parts[2]) + return new RegExp( + parts[1], + parts[2].replace( + new RegExp(`[${forceAddFlags}${forceRemoveFlags}]`, 'g'), + '' + ) + forceAddFlags + ) } - return new RegExp(`^${escape(string)}$`) + return new RegExp(`^${escape(string)}$`, forceAddFlags) } /** @@ -54,7 +65,7 @@ function toRegExpGroupMatcher(patterns = []) { } // In the future, we could optimize this by joining expressions with identical flags. - const regexps = patterns.map(toRegExp) + const regexps = patterns.map((pattern) => toRegExp(pattern, { remove: 'g' })) if (regexps.length === 1) { return (...toCheck) => toCheck.some((str) => regexps[0].test(str)) diff --git a/tests/lib/utils/regexp.js b/tests/lib/utils/regexp.js index 585e054c6..1d98a38c9 100644 --- a/tests/lib/utils/regexp.js +++ b/tests/lib/utils/regexp.js @@ -39,6 +39,68 @@ describe('toRegExp()', () => { assert.deepEqual(toRegExp(`${/^bar/i}`), /^bar/i) assert.deepEqual(toRegExp(`${/[\sA-Z]+/u}`), /[\sA-Z]+/u) }) + + it('should handle simple patterns', () => { + const regex = toRegExp('foo') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), false) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) + + it('should handle simple patterns with added flags', () => { + const regex = toRegExp('foo', { add: 'i' }) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), false) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns', () => { + const regex = toRegExp('/^foo/') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) + + it('should handle regexp patterns with attached flags', () => { + const regex = toRegExp('/^foo/i') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns with added flags', () => { + const regex = toRegExp('/^foo/', { add: 'i' }) + assert.deepEqual(regex, /^foo/i) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns with removed flags', () => { + const regex = toRegExp('/^foo/i', { remove: 'i' }) + assert.deepEqual(regex, /^foo/) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) }) describe('toRegExpCheckGroup()', () => { @@ -76,7 +138,7 @@ describe('toRegExpCheckGroup()', () => { }) it('should return a function for single regexp pattern', () => { - const groupMatcher = toRegExpGroupMatcher(['/^foo/']) + const groupMatcher = toRegExpGroupMatcher(['/^foo/g']) assert.strictEqual(groupMatcher(''), false) assert.strictEqual(groupMatcher('foo'), true) assert.strictEqual(groupMatcher('fooa', 'early'), true) @@ -86,9 +148,9 @@ describe('toRegExpCheckGroup()', () => { }) it('should return a function for multiple regexp patterns', () => { - const groupMatcher = toRegExpGroupMatcher(['/^foo/', '/bar$/']) + const groupMatcher = toRegExpGroupMatcher(['/^foo/', '/bar$/gi']) assert.strictEqual(groupMatcher('foo'), true) - assert.strictEqual(groupMatcher('bar', 'early'), true) + assert.strictEqual(groupMatcher('Bar', 'early'), true) assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) assert.strictEqual(groupMatcher('barfoo'), false) assert.strictEqual(groupMatcher('afoo', 'afooa', 'bara'), false)