Description
🐛 The bug
When maybe() contains named capture groups (created via .as()), magic-regexp fails to correctly wrap the entire content of maybe() as a non-capturing group and make it optional. Instead, it incorrectly makes each component within maybe() individually optional.
This leads to incorrect regex generation and inconsistent matching behavior:
const pattern1 = exactly(
anyOf('beta', 'dev'),
maybe(
charIn('-_.').optionally(),
oneOrMore(digit),
),
)
const pattern2 = exactly(
anyOf('beta', 'dev'),
maybe(
charIn('-_.').optionally(),
oneOrMore(digit).as('number'),
),
)
const testText = '-Beta.zip'
const TEST_RE1 = createRegExp(pattern1, ['g', 'i'])
const TEST_RE2 = createRegExp(pattern2, ['g', 'i'])
console.log(TEST_RE1, testText.match(TEST_RE1)) // -> /(?:beta|dev)(?:(?:[\-_.])?\d+)?/gi [ 'Beta' ] ✅
console.log(TEST_RE2, testText.match(TEST_RE2)) // -> /(?:beta|dev)(?:[\-_.])?(?<number>\d+)?/gi [ 'Beta.' ] ❌
🛠️ To reproduce
https://stackblitz.com/edit/github-qhumtj2p?file=index.mjs
🌈 Expected behaviour
maybe() should wrap all its contents as a whole into a non-capturing group and then make the entire group optional, regardless of whether it contains named capture groups internally.
pattern2 should generate:
// correct
/(?:beta|dev)(?:(?:[\-_.])?(?<number>\d+))?/gi
// Instead of:
/(?:beta|dev)(?:[\-_.])?(?<number>\d+)?/gi
ℹ️ Additional context
The root cause of this issue is that the implementation logic of maybe() doesn't correctly apply the "make the whole thing optional" semantics when handling named capture groups. When it contains named capture groups created by .as(), it makes each internal component optional separately, instead of making the whole unit optional.