Skip to content

feat: add lookbehind support - #59

Open
danshome wants to merge 1 commit into
fent:masterfrom
danshome:feat/lookbehind
Open

danshome wants to merge 1 commit into
fent:masterfrom
danshome:feat/lookbehind

Conversation

@danshome

Copy link
Copy Markdown
Contributor

Refs #13, #57.

What this adds

ret now parses (?<=...) and (?<!...), which the README listed as not supported yet. Before, they threw "Invalid capture group name".

This finishes something the code already anticipated: the Group type has had an unused lookBehind field since the TypeScript conversion (#24), and the lookbehind round-trip tests in test/reconstruct-test.ts were there but commented out.

Token shape

A lookbehind is tokenized like the matching lookahead, plus lookBehind: true:

ret('(?<=a)b')
// { type: ROOT, stack: [
//   { type: GROUP, remember: false, followedBy: true, lookBehind: true, stack: [char a] },
//   char b ] }

I reused followedBy / notFollowedBy instead of adding new flags so that code which already handles lookaheads keeps working: randexp, for example, emits nothing for a group with followedBy or notFollowedBy, so it generates nothing for a lookbehind either. Code that needs to tell them apart checks lookBehind; the README says so and the type documents it. If you'd rather have separate flags, I can change it.

reconstruct emits (?<= and (?<!, so a lookbehind round-trips unchanged.

Details

  • Lookbehinds do not count as capture groups; a capturing group inside one does ((?<!(a))(b)\2 is a reference to group 2).
  • = and ! can never start a group name, so named groups are unaffected. The = / ! must be literal: (?<\x3Da) still throws.
  • Like other zero-width tokens, a quantified lookbehind such as (?<=a)* is not rejected, although JavaScript rejects it (ret already accepts ^* and \b+, which JavaScript also rejects).

README

  • Documents the lookbehind token shape and what may follow (?.
  • Corrects the GROUP examples, which showed stack as an object instead of an array.
  • Moves named groups off the "not supported yet" list, with a note that they are not yet counted when numbering back-references such as \1. \k<name> stays on the list: \k is still read as the literal k.

Testing

  • The commented-out lookaround round-trip tests are enabled and extended (alternation, repetition, back-references, modifier groups, nesting, empty lookbehinds, a neighbouring named group).
  • Parse tests for both forms, nesting both ways, alternation and empty lookbehinds; reference tests for capture numbering; error tests for unterminated, unbalanced and escaped lookbehinds.
  • 382 passing; tsc, eslint and coverage (100%) clean; CI green on Node 20, 22 and 24 on my fork.
  • Independent of my escaped-group-syntax PR (fix: reject escaped characters in group syntax #58): the two merge cleanly in either order, and all tests pass on the combination (387).

Parse `(?<=...)` and `(?<!...)`, which the README listed as not
supported yet, instead of throwing "Invalid capture group name". This
finishes support the code already anticipated: the Group type has had an
unused `lookBehind` field since the TypeScript conversion, and the
lookbehind round-trip tests were present but commented out.

A lookbehind is tokenized like a lookahead (`followedBy` or
`notFollowedBy`, `remember: false`) with `lookBehind: true`, and goes
through the same lookahead branches in the tokenizer. Code that treats
lookaheads as zero-width therefore handles lookbehinds the same way.
Code that needs to tell them apart checks `lookBehind`, which the type
now documents; the README says to check it. Lookbehinds do not count as
capture groups, and `=` and `!` can never start a group name, so named
groups `(?<name>...)` are unaffected. The `=` or `!` must be literal:
`(?<\x3Da)` still throws. Like other zero-width tokens, a quantified
lookbehind such as `(?<=a)*` is not rejected, although JavaScript
rejects it.

`reconstruct` emits `(?<=` and `(?<!` for these groups, so a lookbehind
round-trips unchanged.

Tests: the commented-out lookaround round-trip tests are enabled and
extended (alternation, repetition, back-references, modifier groups,
nesting, empty lookbehinds, a neighbouring named group). Parse tests
cover both forms, nesting both ways, alternation and empty lookbehinds;
reference tests show a lookbehind is not counted as a capture group
while a group inside one is; error tests cover unterminated, unbalanced
and escaped lookbehind syntax.

README: document the lookbehind token shape and what may follow `(?`.
Also correct the GROUP examples, which showed `stack` as an object
rather than an array, and move named groups off the not-supported list
with a note that they are not yet counted when numbering back-references
such as `\1`. `\k<name>` back-references stay on the list: `\k` is still
read as the literal `k`.

Refs fent#13
Refs fent#57

@fent fent left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, thanks again for contributing!

Looks good, just added a few comments in the readme

Comment thread README.md
"stack": [{
"type": ret.types.GROUP,
"stack": { "type": ret.types.CHAR, "value": 97 },
"stack": [{ "type": ret.types.CHAR, "value": 97 }],

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, nice catch 😅

Comment thread README.md
### GROUP

Groups contain tokens that are inside of a parenthesis. If the group begins with `?` followed by another character, it's a special type of group. A ':' tells the group not to be remembered when `exec` is used. '=' means the previous token matches only if followed by this group, and '!' means the previous token matches only if NOT followed.
Groups contain tokens that are inside of a parenthesis. If the group begins with `?` followed by another character, it's a special type of group. A ':' tells the group not to be remembered when `exec` is used. '=' means the previous token matches only if followed by this group, and '!' means the previous token matches only if NOT followed. '<=' means the next token matches only if preceded by this group, and '<!' means it matches only if NOT preceded by it. They set `followedBy` or `notFollowedBy` like a lookahead, plus `lookBehind: true`. Check `lookBehind` before treating `followedBy` or `notFollowedBy` as a lookahead.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is getting a bit long, do you mind making it more into a list?

Comment thread README.md
* `(?<=)` and `(?<!)`: [Negative lookbehind assertions](https://github.com/tc39/proposal-regexp-lookbehind)
* `\k<group>`: [Named group back-references](https://github.com/tc39/proposal-regexp-named-groups)

Named groups `(?<name>...)` are parsed, but they are not yet counted when numbering back-references such as `\1`.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you put place this as part of the previous bullet point?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants