Add prefix, wildcard, and hierarchical glob pattern builders - #12
Merged
Merged
Conversation
Compile a pattern string into an ordinary textCompare "matches" node carrying a pre-compiled regular expression as a textLiteral, so the three common narrower cases stop being hand-written per consumer. prefixPattern enforces a word boundary, so "ls" matches "ls" and "ls -la" but never "lsof". wildcardPattern is a flat dialect where an unescaped * matches any run of characters, \* matches a literal asterisk, \\ a literal backslash, and a pattern whose only wildcard is a trailing " *" also matches the bare prefix. hierarchicalGlobPattern is a segment-aware dialect where * stays inside one /-delimited segment, ** crosses segments, and ? matches one within-segment character. Each is a builder, not a node kind: no schema change, no evaluator branch, and a serialised tree is indistinguishable from a textCompare written out by hand, so three-valued behaviour is inherited rather than re-decided. Compilation happens in a single left-to-right pass rather than by successive replacement over sentinel placeholders, so a pattern that happens to contain a sentinel's own text cannot be corrupted by the restoration step. The compiled pattern is fully anchored and flag-free: "any character" is spelled [\s\S] because the stored string carries no s flag and nothing downstream can add one, and only ECMAScript's SyntaxCharacter set plus / is escaped, which keeps the pattern valid under a u-flagged RegExp as well as an unflagged one.
…sses A glob's `*` and `?` compiled to `[^/]*` and `[^/]`, whose bare `/` is a ClassSetReservedPunctuator in `v` mode and therefore rejected inside a character class. Every `hierarchicalGlobPattern` output containing a wildcard was a SyntaxError under a `v`-flagged RegExp -- in practice nearly all of them, since a glob with no `*` or `?` is just a literal. The builders exist to emit a portable regular-expression string that a consumer compiles itself, and both the module's own escape-set comment and the README state that a compiled pattern stays valid under a `u`- or `v`-flagged RegExp as well as an unflagged one. The glob dialect was the one construct not honouring that: the escape set governs literal characters, and these two class constants bypassed it. `[^\/]` compiles under `v`, `u` and no flags alike, and matches exactly what `[^/]` matched wherever `[^/]` compiled at all, so the stored pattern's meaning is unchanged everywhere it already worked. The existing portability test checked only the `u` flag, which is why this survived: `u` accepts a bare `/` in a class and `v` does not, so `u` alone can never catch it. It now checks unflagged, `u` and `v`, and covers a glob consisting of nothing but a wildcard rather than only one carrying other literals alongside.
Mearman
marked this pull request as ready for review
September 3, 2026 05:29
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
🎉 This PR is included in version 1.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11
Writing the regular expression for a prefix match or a glob by hand is where these go wrong -- get the escape-then-convert ordering backwards and either wildcards stop working, or a literal asterisk sitting in real data silently starts behaving like one. This adds three builder functions that do that compilation once, correctly, so a consumer never writes it again.
prefixPattern(text, prefix)-- word-boundary prefix."ls"matches"ls"and"ls -la", never"lsof". The prefix is a plain literal throughout, so a*in it is just an asterisk.wildcardPattern(text, pattern)-- flat wildcards, no notion of path segments.*is any run of characters,\*a literal asterisk,\\a literal backslash. A pattern whose only unescaped wildcard is a trailing" *"also matches the bare prefix, so"git *"covers"git"as well as"git add file";"git * *"still requires both.hierarchicalGlobPattern(text, pattern)-- segment-aware, for path- or category-tree-shaped values.*stays inside one/-delimited segment,**crosses them,?is one within-segment character. No escape syntax, so a backslash is a backslash.They are deliberately three dialects rather than one function with a mode argument: they answer different questions, and mixing them silently changes what a pattern means.
None of them is a new node kind. Each returns an ordinary
textComparenode withop: "matches"and the compiled regex as atextLiteral, so there is no schema change and no evaluator branch -- a serialised tree is indistinguishable from atextComparesomeone wrote out by hand, and the three-valued behaviour (an unresolvable subject is indeterminate, not a non-match) is inherited rather than re-decided. Ported from the design inMearman/agent-permissions'ssrc/evaluate.ts.Three deliberate divergences from that reference, all behaviour-preserving:
**is consumed as a unit, so"***"still reads as**then*, exactly as the replacement approach paired it.[\s\S]rather than., because the evaluator compiles the stored pattern withnew RegExp(value)and no flags -- nothing downstream can add thesthe reference relies on at its own call site.SyntaxCharacterset plus/. The reference also escapes quotes, which are inert unflagged but aSyntaxErrorunderu.That third point is what the second commit fixes. The
vflag reserves/inside a character class, so the glob dialect's[^/]*and[^/]were aSyntaxErrorunder av-flaggedRegExp-- which is essentially every glob, since one without a*or?is just a literal. The compiled pattern is meant to be portable to whatever the consumer compiles it with, and both the module comment and the README say so explicitly.[^\/]compiles unflagged, underuand underv, matching exactly what[^/]matched wherever it compiled at all. The original portability test only checkedu, which cannot catch this --uaccepts a bare/in a class andvdoes not -- so it now checks all three, against a glob consisting of nothing but a wildcard.Verified by hand beyond the test suite, driving the real
evaluatePredicateend to end rather than eyeballing regex strings: escaped-vs-unescaped asterisks against data containing real asterisks, literal and dangling backslashes, the trailing-wildcard convenience against a bare prefix (including when an escaped\*appears earlier in the same pattern),*-does-not-cross-/versus**-does against strings with real separators, empty patterns and bare*/**/?, regex-injection attempts through the pattern string, and newline-smuggling against the anchors. A sweep over every printable ASCII character, singly and doubled, across all three builders confirms every compiled pattern now parses under no flag,uandvalike.